[
  {
    "path": ".dockerignore",
    "content": ".github\n.git*\n.idea\ndev\ndocs\nlicenses\nnode_modules\npairdrop-cli\n*.md\n*.yml\nDockerfile\nrtc_config_example.json\nturnserver_example.conf"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: schlagmichdoch\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: https://buymeacoffee.com/pairdrop\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug Report\nabout: Create a report to help us improve. Please check the FAQ first.\ntitle: '[Bug] '\nlabels: 'bug'\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Bug occurs on official PairDrop instance https://pairdrop.net/**\nNo | Yes\nVersion: v1.11.2\n\n**Bug occurs on self-hosted PairDrop instance**\nNo | Yes\n\n**Self-Hosted Setup**\nProxy: Nginx | Apache2\nDeployment: docker run | docker compose | npm run start:prod\nVersion: v1.11.2\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/enhancement.md",
    "content": "---\nname: Enhancement\nabout: Enhancements and feature requests are always welcome. See discussions regarding central topics.\ntitle: '[Enhancement] '\nlabels: 'enhancement'\nassignees: ''\n\n---\n\n**What problem is solved by the new feature**\nWhat's the motivation for this topic\n\n**Describe the feature**\nA clear and concise description of what the new feature/enhancement is.\n\n**Drafts**\nScreenshots of Draw.io graph or drawn sketch.\n\n**Additional context**\nAdd any other context here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"npm\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/docker-image.yml",
    "content": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\n# GitHub recommends pinning actions to a commit SHA.\n# To get a newer version, you will need to update the SHA.\n# You can also reference a tag or branch, but the action may change without warning.\n\n# Build a Docker image whenever it is pushed to master\n\nname: Docker Image CI\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n\njobs:\n\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1\n    - name: Build the Docker image\n      run: docker build --pull . -f Dockerfile -t pairdrop\n"
  },
  {
    "path": ".github/workflows/github-image.yml",
    "content": "# This workflow uses actions that are not certified by GitHub.\r\n# They are provided by a third-party and are governed by\r\n# separate terms of service, privacy policy, and support\r\n# documentation.\r\n\r\n# GitHub recommends pinning actions to a commit SHA.\r\n# To get a newer version, you will need to update the SHA.\r\n# You can also reference a tag or branch, but the action may change without warning.\r\n\r\n# Create a Docker image and push it to ghcr.io whenever a new version tag is pushed\r\n\r\nname: GHCR Image CI\r\n\r\non:\r\n  push:\r\n    tags:\r\n      - \"v*.*.*\"\r\n\r\nenv:\r\n  REGISTRY: ghcr.io\r\n  IMAGE_NAME: ${{ github.repository }}\r\n\r\njobs:\r\n  build-and-push-image:\r\n    runs-on: ubuntu-latest\r\n    permissions:\r\n      contents: read\r\n      packages: write\r\n\r\n    steps:\r\n      - name: Checkout repository\r\n        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1\r\n        \r\n      - name: Setup qemu\r\n        uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0\r\n\r\n      - name: Setup Docker Buildx\r\n        uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0\r\n\r\n      - name: Log in to the Container registry\r\n        uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0\r\n        with:\r\n          registry: ${{ env.REGISTRY }}\r\n          username: ${{ github.actor }}\r\n          password: ${{ secrets.GITHUB_TOKEN }}\r\n\r\n      - name: Extract metadata (tags, labels) for Docker\r\n        id: meta\r\n        uses: docker/metadata-action@31cebacef4805868f9ce9a0cb03ee36c32df2ac4 # v5.3.0\r\n        with:\r\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\r\n\r\n      - name: Build and push Docker image\r\n        uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0\r\n        with:\r\n          context: .\r\n          platforms: linux/amd64,linux/arm64\r\n          push: true\r\n          tags: ${{ steps.meta.outputs.tags }}\r\n          labels: ${{ steps.meta.outputs.labels }}\r\n"
  },
  {
    "path": ".github/workflows/zip-release.yml",
    "content": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\n# GitHub recommends pinning actions to a commit SHA.\n# To get a newer version, you will need to update the SHA.\n# You can also reference a tag or branch, but the action may change without warning.\n\n# Create a new zip file from pairdrop-cli whenever a new version tag is pushed\n\nname: Zip Release\n\non:\n  push:\n    tags:\n      - \"v*.*.*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@master\n      - name: Archive Release\n        uses: thedoctor0/zip-release@b57d897cb5d60cb78b51a507f63fa184cfe35554 # v0.7.6\n        with:\n          filename: 'pairdrop-cli.zip'\n          directory: 'pairdrop-cli'\n          exclusions: '*.git* /*node_modules/* .editorconfig'\n      - name: Upload Release\n        uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0\n        with:\n          artifacts: \"pairdrop-cli/pairdrop-cli.zip\"\n          token: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.DS_Store\n/dev/certs\nqrcode-svg/\nturnserver.conf\nrtc_config.json\nssl/\n"
  },
  {
    "path": ".npmrc",
    "content": "engine-strict=true\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Priorities\n- PairDrop should be extremely simple, clean, and easy to use.\n- The main user flow should never be obstructed!\n- New features must be tested thoroughly before we are able to merge them.\n- Stability always comes first!\n\n# Agenda\nPairDrop is a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer.\nWe are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity.\n\nIf you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555).\n\n# Contributing guidelines\nMake sure to follow these guidelines before opening an [issue](https://github.com/schlagmichdoch/pairdrop/issues/new/choose) or a [pull request](https://github.com/schlagmichdoch/pairdrop/pulls):\n\n- Before opening an issue of a pull request, please check if the issue or the pull request already exists.\n- Pull requests for packages updates are not allowed since there is [Dependabot](https://github.com/schlagmichdoch/pairdrop/blob/master/.github/dependabot.yml) that checks them automatically.\n- If you don't know how to contribute, also if you don't know JavaScript or Node.js, you can still share your awesome ideas with a new issue (feature request) and check the whole project for misspellings, too.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine:latest\n\nWORKDIR /home/node/app\n\nCOPY package*.json ./\n\nRUN apk add --no-cache nodejs npm\nRUN NODE_ENV=\"production\" npm ci --omit=dev\n\n# Directories and files excluded via .dockerignore\nCOPY . .\n\n# environment settings\nENV NODE_ENV=\"production\"\n\nEXPOSE 3000\n\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\\n  CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1\n\nENTRYPOINT [\"npm\", \"start\"]"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <a href=\"https://github.com/schlagmichdoch/PairDrop\">\n    <img src=\"public/images/android-chrome-512x512.png\" alt=\"Logo\"  width=\"150\" height=\"150\">\n  </a>\n \n  # _Send it_, with [PairDrop](https://pairdrop.net)\n\n  <p>\n    Local file sharing <a href=\"https://pairdrop.net\"><strong>in your web browser</strong></a>. \n    <br>\n    Inspired by Apple's AirDrop.\n    <br> \n    Fork of Snapdrop.\n    <br>\n    <br>\n    <a href=\"https://github.com/schlagmichdoch/PairDrop/issues\">Report a bug</a>\n    <br />\n    <a href=\"https://github.com/schlagmichdoch/PairDrop/issues\">Request feature</a>\n  </p>\n</div>\n<br>\n\n## Features\nFile sharing on your local network that works on all platforms.\n\n- A multi-platform AirDrop-like solution that works.\n  - Send images, documents or text via peer-to-peer connection to devices on the same local network.\n- Internet transfers\n  - Join temporary public rooms to transfer files easily over the Internet.\n- Web-app \n  - Works on all devices with a modern web-browser.\n \nSend a file from your phone to your laptop?\n<br>Share photos in original quality with friends using Android and iOS?\n<br>Share private files peer-to-peer between Linux systems?\n\n<img src=\"docs/pairdrop_screenshot_mobile.gif\" alt=\"Screenshot GIF showing PairDrop in use\" style=\"width: 300px\">\n\n## Differences to the [Snapdrop](https://github.com/RobinLinus/snapdrop) it is based on\n<details><summary>View all differences</summary>\n\n### Paired Devices and Public Rooms — Internet Transfer\n* Transfer files over the Internet between paired devices or by entering temporary public rooms.\n* Connect to devices in complex network environments (public Wi-Fi, company network, iCloud Private Relay, VPN, etc.).\n* Connect to devices on your mobile hotspot.\n* Devices outside of your local network that are behind a NAT are auto-connected via the PairDrop TURN server.\n* Devices from the local network, in the same public room, or previously paired are shown.\n\n#### Persistent Device Pairing\n\nAlways connect to known devices\n\n* Pair devices via a 6-digit code or a QR-Code.\n* Paired devices always find each other via shared secrets independently of their local network. \n* Pairing is persistent. You find your devices even after reopening PairDrop.\n* You can edit and unpair devices easily.\n\n#### Temporary Public Rooms\n\nConnect to others in complex network situations, or over the Internet.\n\n* Enter a public room via a 5-letter code or a QR-code.\n* Enter a public room to temporarily connect to devices outside your local network.\n* All devices in the same public room see each other.\n* Public rooms are temporary. Closing PairDrop  leaves all rooms.\n\n### [Improved UI for Sending/Receiving Files](https://github.com/RobinLinus/snapdrop/issues/560)\n* Files are transferred after a request is accepted. Files are auto-downloaded upon completing a transfer, if possible.\n* Multiple files are downloaded as a ZIP file\n* Download, share or save to gallery via the \"Share\" menu on Android and iOS.\n* Multiple files are transferred at once with an overall progress indicator.\n\n### Send Files or Text Directly From Share Menu, Context Menu or CLI\n* [Send files directly from context menu on Ubuntu (using Nautilus)](docs/how-to.md#send-multiple-files-and-directories-directly-from-context-menu-on-ubuntu-using-nautilus)\n* [Send files directly from the context menu on Windows](docs/how-to.md#send-files-directly-from-context-menu-on-windows)\n* [Send directly from the \"Share\" menu on iOS](docs/how-to.md#send-directly-from-share-menu-on-ios)\n* [Send directly from the \"Share\" menu on Android](docs/how-to.md#send-directly-from-share-menu-on-android)\n* [Send directly via the command-line interface](docs/how-to.md#send-directly-via-command-line-interface)\n\n### Other Changes\n* Change your display name to easily differentiate your devices.\n* [Paste files/text and choose the recipient afterwards ](https://github.com/RobinLinus/snapdrop/pull/534)\n* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)\n* Warn user before PairDrop is closed on file transfer\n* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))\n* [Video and audio preview](https://github.com/RobinLinus/snapdrop/pull/455) (Thanks [@victorwads](https://github.com/victorwads))\n* Switch theme back to auto/system after dark or light mode is on\n* Node-only implementation (Thanks [@Bellisario](https://github.com/Bellisario))\n* Auto-restart on error (Thanks [@KaKi87](https://github.com/KaKi87))\n* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))\n* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)\n* When hosting PairDrop yourself, you can [set your own STUN/TURN servers](docs/host-your-own.md#specify-stunturn-servers)\n* Translations.\n\n</details>\n\n## Translate PairDrop on [Hosted Weblate](https://hosted.weblate.org/engage/pairdrop/)\n<a href=\"https://hosted.weblate.org/engage/pairdrop/\">\n<img src=\"https://hosted.weblate.org/widget/pairdrop/horizontal-blue.svg\" alt=\"Translation status\" style=\"width: 300px\" />\n</a>\n\n## Built with the following awesome technologies:\n* Vanilla HTML5 / JS ES6 / CSS 3 frontend\n* [WebRTC](http://webrtc.org/) / WebSockets\n* [Node.js](https://nodejs.org/en/) backend\n* [Progressive web app (PWA)](https://en.wikipedia.org/wiki/Progressive_web_app) unified functionality\n* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) storage handling\n* [zip.js](https://gildas-lormeau.github.io/zip.js/) library\n* [cyrb53](https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js) super-fast hash function\n* [NoSleep](https://github.com/richtr/NoSleep.js) display sleep, add wake lock ([MIT](licenses/MIT-NoSleep))\n* [heic2any](https://github.com/alexcorvi/heic2any) HEIC/HEIF to PNG/GIF/JPEG ([MIT](licenses/MIT-heic2any))\n* [Weblate](https://weblate.org/) web-based localization tool\n* [BrowserStack](https://www.browserstack.com/) This project is tested with BrowserStack\n\n[FAQ](docs/faq.md)\n\n[Host your own instance with Docker or Node.js](docs/host-your-own.md).\n\n## Support\n<a href=\"https://www.buymeacoffee.com/pairdrop\" target=\"_blank\">\n<img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-blue.png\" alt=\"Buy me a coffee\" style=\"height: 60px !important;width: 217px !important;\" >\n</a>\n<br />\n<br />\n\nPairDrop is libre, and always will be. \\\nIf you find it useful and want to support free and open-source software, please consider donating using the button above. \\\nI footed the bill for the domain and the server, and you can help create and maintain great software by supporting me. \\\nThank you very much for your contribution!\n\n## Contributing\nFeel free to [open an issue](https://github.com/schlagmichdoch/pairdrop/issues/new/choose) or a\n[pull request](https://github.com/schlagmichdoch/pairdrop/pulls), following the\n[Contributing Guidelines](CONTRIBUTING.md).\n"
  },
  {
    "path": "dev/nginx/default.conf",
    "content": "server {\n    listen       80;\n\n    expires epoch;\n\n    location / {\n        proxy_connect_timeout 300;\n        proxy_pass http://pairdrop:3000;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Upgrade $http_upgrade;\n    }\n\n    location /ca.crt {\n        alias /etc/ssl/certs/pairdropCA.crt;\n    }\n\n    # To allow POST on static pages\n    error_page  405     =200 $uri;\n}\n\nserver {\n    listen 443 ssl;\n    http2 on;\n    ssl_certificate /etc/ssl/certs/pairdrop-dev.crt;\n    ssl_certificate_key /etc/ssl/certs/pairdrop-dev.key;\n\n    expires epoch;\n\n    location / {\n        proxy_connect_timeout 300;\n        proxy_pass http://pairdrop:3000;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Upgrade $http_upgrade;\n    }\n\n    location /ca.crt {\n        alias /etc/ssl/certs/pairdropCA.crt;\n    }\n    # To allow POST on static pages\n    error_page  405 =200 $uri;\n}\n\n"
  },
  {
    "path": "dev/nginx-with-openssl.Dockerfile",
    "content": "FROM nginx:alpine\n\nRUN apk add --no-cache openssl"
  },
  {
    "path": "dev/openssl/create.sh",
    "content": "#!/bin/sh\n\ncnf_dir='/mnt/openssl/'\ncerts_dir='/etc/ssl/certs/'\nopenssl req -config ${cnf_dir}pairdropCA.cnf -new -x509 -days 1 -keyout ${certs_dir}pairdropCA.key -out ${certs_dir}pairdropCA.crt\nopenssl req -config ${cnf_dir}pairdropCert.cnf -new -out /tmp/pairdrop-dev.csr -keyout ${certs_dir}pairdrop-dev.key\nopenssl x509 -req -in /tmp/pairdrop-dev.csr -CA ${certs_dir}pairdropCA.crt -CAkey ${certs_dir}pairdropCA.key -CAcreateserial -extensions req_ext -extfile ${cnf_dir}pairdropCert.cnf -sha512 -days 1 -out ${certs_dir}pairdrop-dev.crt\n\nexec \"$@\"\n"
  },
  {
    "path": "dev/openssl/pairdropCA.cnf",
    "content": "[ req ]\ndefault_bits       = 2048\ndefault_md         = sha256\ndefault_days       = 1\nencrypt_key        = no\ndistinguished_name = subject\nx509_extensions    = x509_ext\nstring_mask        = utf8only\nprompt             = no\n\n[ subject ]\norganizationName = PairDrop\nOU               = CA\ncommonName       = pairdrop-CA\n\n[ x509_ext ]\nsubjectKeyIdentifier      = hash\nauthorityKeyIdentifier    = keyid:always,issuer\n\n# You only need digitalSignature below. *If* you don't allow\n#   RSA Key transport (i.e., you use ephemeral cipher suites), then\n#   omit keyEncipherment because that's key transport.\n\nbasicConstraints = critical, CA:TRUE, pathlen:0\nkeyUsage         = critical, digitalSignature, keyEncipherment, cRLSign, keyCertSign\n\n"
  },
  {
    "path": "dev/openssl/pairdropCert.cnf",
    "content": "[ req ]\ndefault_bits        = 2048\ndefault_md          = sha256\ndefault_days        = 1\nencrypt_key         = no\ndistinguished_name  = subject\nreq_extensions      = req_ext\nstring_mask         = utf8only\nprompt              = no\n\n[ subject ]\norganizationName    = PairDrop\nOU                  = Development\n\n# Use a friendly name here because it's presented to the user. The server's DNS\n#   names are placed in Subject Alternate Names. Plus, DNS names here is deprecated\n#   by both IETF and CA/Browser Forums. If you place a DNS name here, then you\n#   must include the DNS name in the SAN too (otherwise, Chrome and others that\n#   strictly follow the CA/Browser Baseline Requirements will fail).\n\ncommonName           = ${ENV::FQDN}\n\n[ req_ext ]\nsubjectKeyIdentifier = hash\nbasicConstraints     = CA:FALSE\nkeyUsage             = digitalSignature, keyEncipherment\nsubjectAltName       = DNS:${ENV::FQDN}\nnsComment            = \"OpenSSL Generated Certificate\"\nextendedKeyUsage     = serverAuth\n"
  },
  {
    "path": "docker-compose-coturn.yml",
    "content": "version: \"3\"\nservices:\n  pairdrop:\n    image: \"lscr.io/linuxserver/pairdrop:latest\"\n    container_name: pairdrop\n    restart: unless-stopped\n    volumes:\n      - ./rtc_config.json:/home/node/app/rtc_config.json\n    environment:\n      - PUID=1000 # UID to run the application as\n      - PGID=1000 # GID to run the application as\n      - WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.\n      - RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.\n      - RTC_CONFIG=/home/node/app/rtc_config.json # Set to the path of a file that specifies the STUN/TURN servers.\n      - DEBUG_MODE=false # Set to true to debug container and peer connections.\n      - TZ=Etc/UTC # Time Zone\n    ports:\n      - \"127.0.0.1:3000:3000\" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000`\n  coturn_server:\n    image: \"coturn/coturn\"\n    restart: unless-stopped\n    volumes:\n      - ./turnserver.conf:/etc/coturn/turnserver.conf\n      - ./ssl/:/etc/coturn/ssl/\n    ports:\n      - \"3478:3478\"\n      - \"3478:3478/udp\"\n      - \"5349:5349\"\n      - \"5349:5349/udp\"\n      - \"10000-20000:10000-20000/udp\"\n    # see guide at docs/host-your-own.md#coturn-and-pairdrop-via-docker-compose"
  },
  {
    "path": "docker-compose-dev.yml",
    "content": "version: \"3\"\nservices:\n  pairdrop:\n    build: .\n    container_name: pairdrop\n    restart: unless-stopped\n    environment:\n      - PUID=1000 # UID to run the application as\n      - PGID=1000 # GID to run the application as\n      - WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.\n      - RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.\n      - RTC_CONFIG=false # Set to the path of a file that specifies the STUN/TURN servers.\n      - DEBUG_MODE=false # Set to true to debug container and peer connections.\n      - TZ=Etc/UTC # Time Zone\n    ports:\n      - \"127.0.0.1:3000:3000\" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000`\n  nginx:\n    build:\n      context: dev/\n      dockerfile: nginx-with-openssl.Dockerfile\n    image: \"nginx-with-openssl\"\n    volumes:\n      - ./public:/usr/share/nginx/html\n      - ./dev/certs:/etc/ssl/certs\n      - ./dev/openssl:/mnt/openssl\n      - ./dev/nginx/default.conf:/etc/nginx/conf.d/default.conf\n    ports:\n      - \"8080:80\"\n      - \"8443:443\"\n    environment:\n      - FQDN=localhost\n    entrypoint: /mnt/openssl/create.sh\n    command: [\"nginx\", \"-g\", \"daemon off;\"]\n    restart: unless-stopped"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\nservices:\n  pairdrop:\n    image: \"lscr.io/linuxserver/pairdrop:latest\"\n    container_name: pairdrop\n    restart: unless-stopped\n    environment:\n      - PUID=1000 # UID to run the application as\n      - PGID=1000 # GID to run the application as\n      - WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.\n      - RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.\n      - RTC_CONFIG=false # Set to the path of a file that specifies the STUN/TURN servers.\n      - DEBUG_MODE=false # Set to true to debug container and peer connections.\n      - TZ=Etc/UTC # Time Zone\n    ports:\n      - \"127.0.0.1:3000:3000\" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000`\n"
  },
  {
    "path": "docs/docker-swarm-usage.md",
    "content": "# Docker Swarm Usage\n\n## Healthcheck\n\nThe [Docker Image](../Dockerfile) includes a health check with the following options:\n\n```\n--interval=30s\n```\n> Specifies the time interval to run the health check. \\\n> In this case, the health check is performed every 30 seconds.\n<br>\n\n```\n--timeout=10s\n```\n> Specifies the amount of time to wait for a response from the \\\"HEALTHCHECK\\\" command. \\\n> If the response does not arrive within 10 seconds, the health check fails.\n<br>\n\n```\n--start-period=5s\n```\n> Specifies the amount of time to wait before starting the health check process. \\\n> In this case, the health check process will begin 5 seconds after the container is started.\n<br>\n\n```\n--retries=3\n```\n> Specifies the number of times Docker should retry the health check \\\n> before considering the container to be unhealthy.\n<br>\n\n\nThe CMD instruction is used to define the command that will be run as part of the health check. \\\nIn this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. \\\nThis command will attempt to connect to `http://localhost:3000/` \\\nand if it fails it will exit with a status code of `1`. \\\nIf this command returns a status code other than `0`, the health check fails.\n\nOverall, this \\\"HEALTHCHECK\\\" instruction is defining a health check process \\\nthat runs every 30 seconds, and waits up to 10 seconds for a response, \\\nbegins 5 seconds after the container is started, and retries up to 3 times. \\ \nThe health check attempts to connect to http://localhost:3000/ \\\nand will considers the container unhealthy if unable to connect.\n\n"
  },
  {
    "path": "docs/faq.md",
    "content": "# Frequently Asked Questions\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    Help! I can't install the PWA!\n</summary>\n\n<br>\n\nHere is a good guide on how to install PWAs on different platforms: \\\nhttps://www.cdc.gov/niosh/mining/content/hearingloss/installPWA.html\n\n\n**Chromium-based browser on Desktop (Chrome, Edge, Vivaldi, Brave, etc.)** \\\nEasily install PairDrop PWA on your desktop by clicking the install-button in the top-right corner while on [pairdrop.net](https://pairdrop.net).\n\n<img width=\"400\" src=\"pwa-install.png\" alt=\"Example on how to install a pwa with Edge\">\n\n**Desktop Firefox** \\\nOn Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/)\n\n**Android** \\\nPWAs are installable only by using Google Chrome or Samsung Browser:\n1. Visit [pairdrop.net](https://pairdrop.net)\n2. Click _Install_ on the installation pop-up or use the three-dot-menu and click on _Add to Home screen_\n3. Click _Add_ on the pop-up\n\n**iOS** \\\nPWAs are installable only by using Safari:\n1. Visit [pairdrop.net](https://pairdrop.net)\n2. Click on the share icon\n3. Click _Add to Home Screen_\n4. Click _Add_ in the top right corner\n\n<br>\n\n**Self-Hosted Instance?** \\\nTo be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs).\nSee [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more info.\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    Shortcuts?\n</summary>\n\n<br>\n\nAvailable shortcuts:\n- Send a message with `CTRL + ENTER`\n- Close all \"Send\" and \"Pair\" dialogs by pressing `Esc`.\n- Copy a received message to the clipboard with `CTRL/⌘ + C`.\n- Accept file-transfer requests with `Enter` and decline with `Esc`.\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    How to save images directly to the gallery on iOS?\n</summary>\n\n<br>\n\n~~Apparently, iOS does not allow images shared from a website to be saved to the gallery directly.~~\n~~It simply does not offer that option for images shared from a website.~~\n\n~~iOS Shortcuts saves the day:~~ \\\nI created a simple iOS shortcut that takes your photos and saves them to your gallery:\nhttps://routinehub.co/shortcut/13988/\n\nUpdate: \\\nApparently, this was only a bug that is fixed in recent iOS version (https://github.com/WebKit/WebKit/pull/13111). \\\nIf you use an older affected iOS version this might still be of use. \\\nLuckily, you can now simply use `Save Image`/`Save X Images` 🎉\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    Is it possible to send files or text directly from the \"Context\" or \"Share\" menu?\n</summary>\n\n<br>\n\nYes, it finally is.\n* [Send files directly from the \"Context\" menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)\n* [Send directly from the \"Share\" menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)\n* [Send directly from the \"Share\" menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    Is it possible to send files or text directly via CLI?\n</summary>\n\n<br>\n\nYes.\n\n* [Send directly from a command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    Are there any third-party Apps?\n</summary>\n\n<br>\n\nThese third-party apps are compatible with PairDrop:\n\n1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android)\n2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon)\n3. Feel free to make one :)\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    What about the connection? Is it a P2P connection directly from device to device or is there any third-party-server?\n</summary>\n\n<br>\n\nIt uses a WebRTC peer-to-peer connection.\nWebRTC needs a signaling server that is only used to establish a connection.\nThe server is not involved in the file transfer.\n\nIf the devices are on the same network,\nnone of your files are ever sent to any server.\n\nIf your devices are paired and behind a NAT,\nthe PairDrop TURN Server is used to route your files and messages.\nSee the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn)\nto learn more about STUN, TURN and WebRTC.\n\nIf you host your own instance\nand want to support devices that do not support WebRTC,\nyou can [start the PairDrop instance with an activated WebSocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn).\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    What about privacy? Will files be saved on third-party servers?\n</summary>\n\n<br>\n\nFiles are sent directly between peers.\nPairDrop doesn't even use a database.\nIf curious, study [the signaling server](https://github.com/schlagmichdoch/PairDrop/blob/master/server/ws-server.js).\nWebRTC encrypts the files in transit.\n\nIf the devices are on the same network,\nnone of your files are ever sent to any server.\n\nIf your devices are paired and behind a NAT,\nthe PairDrop TURN Server is used to route your files and messages.\nSee the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn)\nto learn more about STUN, TURN and WebRTC.\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    What about security? Are my files encrypted while sent between the computers?\n</summary>\n\n<br>\n\nYes. Your files are sent using WebRTC, encrypting them in transit.\nStill you have to trust the PairDrop server. To ensure the connection is secure and there is no [MITM](https://en.m.wikipedia.org/wiki/Man-in-the-middle_attack) there is a plan to make PairDrop\nzero trust by encrypting the signaling and implementing a verification process. See [issue #180](https://github.com/schlagmichdoch/PairDrop/issues/180) to keep updated.\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    Transferring many files with paired devices takes too long\n</summary>\n\n<br>\n\nNaturally, if traffic needs to be routed through the TURN server\nbecause your devices are behind different NATs, transfer speed decreases.\n\nYou can open a hotspot on one of your devices to bridge the connection,\nwhich omits the need of the TURN server.\n\n- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)\n- [How to open a hotspot on macOS](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)\n- [Library to open a hotspot on Linux](https://github.com/lakinduakash/linux-wifi-hotspot)\n\nYou can also use mobile hotspots on phones to do that. \nThen, all data should be sent directly between devices and not use your data plan.\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    Why don't you implement feature xyz?\n</summary>\n\n<br>\n\nSnapdrop and PairDrop are a study in radical simplicity.\nThe user interface is insanely simple.\nFeatures are chosen very carefully because complexity grows quadratically\nsince every feature potentially interferes with each other feature.\nWe focus very narrowly on a single use case: instant file transfer. \nNot facilitating optimal edge-cases means better flow for average users.\nDon't be sad. We may decline your feature request for the sake of simplicity. \n\nRead *Insanely Simple: The Obsession that Drives Apple's Success*,\nand/or *Thinking, Fast and Slow* to learn more.\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    PairDrop is awesome. How can I support it? \n</summary>\n\n<br>\n\n* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support libre software.\n* [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues)\n* Share PairDrop on social media.\n* Fix bugs and create a pull request.\n* Do some security analysis and make suggestions.\n* Participate in [active discussions](https://github.com/schlagmichdoch/PairDrop/discussions)\n\n<br>\n\n</details>\n\n<details>\n<summary style=\"font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;\">\n    How does it work?\n</summary>\n\n<br>\n\n[See here for info about the technical implementation](/docs/technical-documentation.md)\n\n<br>\n\n</details>\n\n[< Back](/README.md)\n"
  },
  {
    "path": "docs/host-your-own.md",
    "content": "# Deployment Notes\n\n## TURN server for Internet Transfer\n\nBeware that you have to host your own TURN server to enable transfers between different networks.\n\nFollow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) \nor deploy it via Docker (Step 5).\n\nYou can use the `docker-compose-coturn.yml` in this repository. See [Coturn and PairDrop via Docker Compose](#coturn-and-pairdrop-via-docker-compose).\n \nAlternatively, use a free, pre-configured TURN server like [OpenRelay](https://www.metered.ca/tools/openrelay/)\n\n<br>\n\n## PairDrop via HTTPS\n\nOn some browsers PairDrop must be served over TLS in order for some features to work properly.\nThese may include:\n- Copying an incoming message via the 'copy' button\n- Installing PairDrop as PWA\n- Persistent pairing of devices\n- Changing of the display name\n- Notifications\n\nNaturally, this is also recommended to increase security.\n\n<br>\n\n## Deployment with Docker\n\nThe easiest way to get PairDrop up and running is by using Docker.\n\n### Docker Image from Docker Hub\n\n```bash\ndocker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop\n```\n> This image is hosted by [linuxserver.io](https://linuxserver.io). For more information visit https://hub.docker.com/r/linuxserver/pairdrop\n\n\n<br>\n\n### Docker Image from GitHub Container Registry (ghcr.io)\n\n```bash\ndocker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop\n```\n\n\n<br>\n\n### Docker Image self-built\n\n#### Build the image\n\n```bash\ndocker build --pull . -f Dockerfile -t pairdrop\n```\n\n> A GitHub action is set up to do this step automatically at the release of new versions.\n>\n> `--pull` ensures always the latest node image is used.\n\n#### Run the image\n\n```bash\ndocker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop\n```\n\n> You must use a server proxy to set the `X-Forwarded-For` header \n> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).\n>\n> To prevent bypassing the proxy by reaching the docker container directly, \n> `127.0.0.1` is specified in the run command.\n\n\n<br>\n\n### Flags\n\nSet options by using the following flags in the `docker run` command:\n\n#### Port\n\n```bash\n-p 127.0.0.1:8080:3000\n```\n\n> Specify the port used by the docker image\n>\n> - 3000 -> `-p 127.0.0.1:3000:3000`\n> - 8080 -> `-p 127.0.0.1:8080:3000`\n\n#### Set Environment Variables via Docker\n\nEnvironment Variables are set directly in the `docker run` command: \\\ne.g. `docker run -p 127.0.0.1:3000:3000 -it pairdrop -e DEBUG_MODE=\"true\"`\n\nOverview of available Environment Variables are found [here](#environment-variables).\n\nExample:\n```bash\ndocker run -d \\\n    --name=pairdrop \\\n    --restart=unless-stopped \\\n    -p 127.0.0.1:3000:3000 \\\n    -e PUID=1000 \\\n    -e PGID=1000 \\\n    -e WS_SERVER=false \\\n    -e WS_FALLBACK=false \\\n    -e RTC_CONFIG=false \\\n    -e RATE_LIMIT=false \\\n    -e DEBUG_MODE=false \\\n    -e TZ=Etc/UTC \\\n    lscr.io/linuxserver/pairdrop \n```\n\n<br>\n\n## Deployment with Docker Compose\n\nHere's an example docker compose file:\n\n```yaml\nversion: \"3\"\nservices:\n    pairdrop:\n        image: \"lscr.io/linuxserver/pairdrop:latest\"\n        container_name: pairdrop\n        restart: unless-stopped\n        environment:\n            - PUID=1000 # UID to run the application as\n            - PGID=1000 # GID to run the application as\n            - WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.\n            - RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.\n            - RTC_CONFIG=false # Set to the path of a file that specifies the STUN/TURN servers.\n            - DEBUG_MODE=false # Set to true to debug container and peer connections.\n            - TZ=Etc/UTC # Time Zone\n        ports:\n            - \"127.0.0.1:3000:3000\" # Web UI\n```\n\nRun the compose file with `docker compose up -d`.\n\n> You must use a server proxy to set the `X-Forwarded-For` header\n> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).\n>\n> To prevent bypassing the proxy by reaching the Docker container \n> directly, `127.0.0.1` is specified in the `ports` argument.\n\n<br>\n\n## Deployment with Node.js\n\nClone this repository and enter the folder\n\n```bash\ngit clone https://github.com/schlagmichdoch/PairDrop.git && cd PairDrop\n```\n\nInstall all dependencies with NPM:\n\n```bash\nnpm install\n```\n\nStart the server with:\n\n```bash\nnpm start\n```\n\n> By default, the node server listens on port 3000.\n\n\n<br>\n\n### Options / Flags\n\nThese are some flags only reasonable when deploying via Node.js\n\n#### Port\n\n```bash\nPORT=3000\n```\n\n> Default: `3000`\n> \n> Environment variable to specify the port used by the Node.js server \\\n> e.g. `PORT=3010 npm start`\n\n#### Local Run\n\n```bash\nnpm start -- --localhost-only\n```\n\n> Only allow connections from localhost.\n>\n> You must use a server proxy to set the `X-Forwarded-For` header \n> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).\n>\n> Use this when deploying PairDrop with node to prevent \n> bypassing the reverse proxy by reaching the Node.js server directly.\n\n#### Automatic restart on error\n\n```bash\nnpm start -- --auto-restart\n```\n\n> Restarts server automatically on error\n\n#### Production (autostart and rate-limit)\n\n```bash\nnpm run start:prod\n```\n\n> shortcut for `RATE_LIMIT=5 npm start -- --auto-restart`\n\n#### Production (autostart, rate-limit, localhost-only)\n\n```bash\nnpm run start:prod -- --localhost-only\n```\n\n> To prevent connections to the node server from bypassing \\\n> the proxy server you should always use \"--localhost-only\" on production.\n\n#### Set Environment Variables via Node.js\n\nTo specify environment variables set them in the run command in front of `npm start`.\nThe syntax is different on Unix and Windows.\n\nOn Unix based systems\n\n```bash\nPORT=3000 RTC_CONFIG=\"rtc_config.json\" npm start\n```\n\nOn Windows\n\n```bash\n$env:PORT=3000 RTC_CONFIG=\"rtc_config.json\"; npm start\n```\n\nOverview of available Environment Variables are found [here](#environment-variables).\n\n<br>\n\n## Environment Variables\n\n### Debug Mode\n\n```bash\nDEBUG_MODE=\"true\"\n```\n\n> Default: `false`\n>\n> Logs the used environment variables for debugging.\n>\n> Prints debugging information about the connecting peers IP addresses.\n> \n> This is quite useful to check whether the [#HTTP-Server](#http-server)\n> is configured correctly, so the auto-discovery feature works correctly.\n> Otherwise, all clients discover each other mutually, independently of their network status.\n>\n> If this flag is set to `\"true\"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:\n>\n> ```\n> ----DEBUGGING-PEER-IP-START----\n> remoteAddress: ::ffff:172.17.0.1\n> x-forwarded-for: 19.117.63.126\n> cf-connecting-ip: undefined\n> PairDrop uses: 19.117.63.126\n> IP is private: false\n> if IP is private, '127.0.0.1' is used instead\n> ----DEBUGGING-PEER-IP-END----\n> ```\n>\n> If the IP address \"PairDrop uses\" matches the public IP address of the client device, everything is set up correctly. \\\n> To find out the public IP address of the client device visit https://whatsmyip.com/.\n>\n> To preserve your clients' privacy: \\\n> **Never use this environment variable in production!**\n\n\n<br>\n\n### Rate limiting requests\n\n```bash\nRATE_LIMIT=1\n```\n\n> Default: `false`\n>\n> Limits clients to 1000 requests per 5 min\n>\n> \"If you are behind a proxy/load balancer (usually the case with most hosting services, e.g. Heroku, Bluemix, AWS ELB,\n> Render, Nginx, Cloudflare, Akamai, Fastly, Firebase Hosting, Rackspace LB, Riverbed Stingray, etc.), the IP address of\n> the request might be the IP of the load balancer/reverse proxy (making the rate limiter effectively a global one and\n> blocking all requests once the limit is reached) or undefined.\"\n> (See: https://express-rate-limit.mintlify.app/guides/troubleshooting-proxy-issues)\n>\n> To find the correct number to use for this setting:\n>\n> 1. Start PairDrop with `DEBUG_MODE=True` and `RATE_LIMIT=1`\n> 2. Make a `get` request to `/ip` of the PairDrop instance (e.g. `https://pairdrop-example.net/ip`)\n> 3. Check if the IP address returned in the response matches your public IP address (find out by visiting e.g. https://whatsmyip.com/)\n> 4. You have found the correct number if the IP addresses match. If not, then increase `RATE_LIMIT` by one and redo 1. - 4.\n>\n> e.g. on Render you must use RATE_LIMIT=5\n\n\n<br>\n\n### IPv6 Localization\n\n```bash\nIPV6_LOCALIZE=4\n```\n\n> Default: `false`\n>\n> To enable Peer Auto-Discovery among IPv6 peers, you can specify a reduced number of segments \\\n> of the client IPv6 address to be evaluated as the peer's IP. \\\n> This can be especially useful when using Cloudflare as a proxy.\n>\n> The flag must be set to an **integer** between `1` and `7`. \\\n> The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) \\\n> to match the client IP against. The most common value would be `4`, \\\n> which will group peers within the same `/64` subnet.\n\n\n<br>\n\n### Websocket Fallback (for VPN)\n\n```bash\nWS_FALLBACK=true\n```\n\n> Default: `false`\n>\n> Provides PairDrop to clients with an included websocket fallback \\\n> if the peer to peer WebRTC connection is not available to the client.\n>\n> This is not used on the official https://pairdrop.net website, \n> but you can activate it on your self-hosted instance.\\\n> This is especially useful if you connect to your instance via a VPN (as most VPN services block WebRTC completely in \n> order to hide your real IP address). ([Read more here](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).\n>\n> **Warning:** \\\n> All traffic sent between devices using this fallback\n> is routed through the server and therefor not peer to peer!\n> \n> Beware that the traffic routed via this fallback is readable by the server. \\\n> Only ever use this on instances you can trust.\n> \n> Additionally, beware that all traffic using this fallback debits the servers data plan.\n\n\n<br>\n\n### Specify STUN/TURN Servers\n\n```bash\nRTC_CONFIG=\"rtc_config.json\"\n```\n\n> Default: `false`\n>\n> Specify the STUN/TURN servers PairDrop clients use by setting \\\n> `RTC_CONFIG` to a JSON file including the configuration. \\\n> You can use `rtc_config_example.json` as a starting point.\n>\n> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/\n> Alternatively, use a free, pre-configured TURN server like [OpenRelay](<[url](https://www.metered.ca/tools/openrelay/)>)\n>\n> Default configuration:\n>\n> ```json\n> {\n>   \"sdpSemantics\": \"unified-plan\",\n>   \"iceServers\": [\n>     {\n>       \"urls\": \"stun:stun.l.google.com:19302\"\n>     }\n>   ]\n> }\n> ```\n\n<br>\n\nYou can host an instance that uses another signaling server\nThis 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.\n\n### Specify Signaling Server\n\n```bash\nSIGNALING_SERVER=\"pairdrop.net\"\n```\n\n> Default: `false`\n>\n> By default, clients connecting to your instance use the signaling server of your instance to connect to other devices.\n> \n> By using `SIGNALING_SERVER`, you can host an instance that uses another signaling server.\n> \n> This can be useful if you want to ensure the integrity of the client files and don't want to trust the client files that are hosted on another PairDrop instance but still want to connect to devices that use the other instance.\n> E.g. host your own client files under *pairdrop.your-domain.com* but use the official signaling server under *pairdrop.net*\n> This way devices connecting to *pairdrop.your-domain.com* and *pairdrop.net* can discover each other.\n> \n> Beware that the version of your PairDrop server must be compatible with the version of the signaling server.\n>\n> `SIGNALING_SERVER` must be a valid url without the protocol prefix. \n> Examples of valid values: `pairdrop.net`, `pairdrop.your-domain.com:3000`, `your-domain.com/pairdrop`\n\n<br>\n\n### Customizable buttons for the _About PairDrop_ page\n\n```bash\nDONATION_BUTTON_ACTIVE=true\nDONATION_BUTTON_LINK=\"https://www.buymeacoffee.com/pairdrop\"\nDONATION_BUTTON_TITLE=\"Buy me a coffee\"\nTWITTER_BUTTON_ACTIVE=true\nTWITTER_BUTTON_LINK=\"https://twitter.com/account\"\nTWITTER_BUTTON_TITLE=\"Find me on Twitter\"\nMASTODON_BUTTON_ACTIVE=true\nMASTODON_BUTTON_LINK=\"https://mastodon.social/account\"\nMASTODON_BUTTON_TITLE=\"Find me on Mastodon\"\nBLUESKY_BUTTON_ACTIVE=true\nBLUESKY_BUTTON_LINK=\"https://bsky.app/profile/account\"\nBLUESKY_BUTTON_TITLE=\"Find me on Bluesky\"\nCUSTOM_BUTTON_ACTIVE=true\nCUSTOM_BUTTON_LINK=\"https://your-custom-social-network.net/account\"\nCUSTOM_BUTTON_TITLE=\"Find me on this custom social network\"\nPRIVACYPOLICY_BUTTON_ACTIVE=true\nPRIVACYPOLICY_BUTTON_LINK=\"https://link-to-your-privacy-policy.net\"\nPRIVACYPOLICY_BUTTON_TITLE=\"Open our privacy policy\"\n```\n\n> Default: unset\n>\n> By default, clients will show the default button configuration: GitHub, BuyMeACoffee, Twitter, and FAQ on GitHub.\n> \n> The GitHub and FAQ on GitHub buttons are essential, so they are always shown.\n> \n> The other buttons can be customized:\n>\n> * `*_BUTTON_ACTIVE`: set this to `true` to show a natively hidden button or to `false` to hide a normally shown button\n> * `*_BUTTON_LINK`: set this to any URL to overwrite the href attribute of the button\n> * `*_BUTTON_TITLE`: set this to overwrite the hover title of the button. This will prevent the title from being translated.\n\n<br>\n\n## Healthcheck\n\n> The Docker Image hosted on `ghcr.io` and the self-built Docker Image include a healthcheck.\n>\n> Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).\n\n<br>\n\n## HTTP-Server\n\nWhen running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. \\\nOtherwise, all clients will be mutually visible.\n\nTo check if your setup is configured correctly [use the environment variable `DEBUG_MODE=\"true\"`](#debug-mode).\n\n### Using nginx\n\n#### Allow http and https requests\n\n```\nserver {\n    listen       80;\n\n    expires epoch;\n\n    location / {\n        proxy_connect_timeout 300;\n        proxy_pass http://127.0.0.1:3000;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header X-Forwarded-for $remote_addr;\n    }\n}\n\nserver {\n    listen       443 ssl http2;\n    ssl_certificate /etc/ssl/certs/pairdrop-dev.crt;\n    ssl_certificate_key /etc/ssl/certs/pairdrop-dev.key;\n\n    expires epoch;\n\n    location / {\n        proxy_connect_timeout 300;\n        proxy_pass http://127.0.0.1:3000;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header X-Forwarded-for $remote_addr;\n    }\n}\n```\n\n#### Automatic http to https redirect:\n\n```\nserver {\n    listen       80;\n\n    expires epoch;\n\n    location / {\n        return 301 https://$host:3000$request_uri;\n    }\n}\n\nserver {\n    listen       443 ssl http2;\n    ssl_certificate /etc/ssl/certs/pairdrop-dev.crt;\n    ssl_certificate_key /etc/ssl/certs/pairdrop-dev.key;\n\n    expires epoch;\n\n    location / {\n        proxy_connect_timeout 300;\n        proxy_pass http://127.0.0.1:3000;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header X-Forwarded-for $remote_addr;\n    }\n}\n```\n\n\n<br>\n\n### Using Apache\n\ninstall modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`\n\n```bash\na2enmod proxy\n```\n\n```bash\na2enmod proxy_http\n```\n\n<br>\n\nCreate a new configuration file under `/etc/apache2/sites-available` (on Debian)\n\n**pairdrop.conf**\n\n#### Allow HTTP and HTTPS requests\n\n```apacheconf\n<VirtualHost *:80>\n\tProxyPass / http://127.0.0.1:3000/ upgrade=websocket\n</VirtualHost>\n<VirtualHost *:443>\n\tProxyPass / https://127.0.0.1:3000/ upgrade=websocket\n</VirtualHost>\n```\n\n#### Automatic HTTP to HTTPS redirect:\n\n```apacheconf\n<VirtualHost *:80>\n\tRedirect permanent / https://127.0.0.1:3000/\n</VirtualHost>\n<VirtualHost *:443>\n\tProxyPass / http://127.0.0.1:3000/ upgrade=websocket\n</VirtualHost>\n```\n\nActivate the new virtual host and reload Apache:\n\n```bash\na2ensite pairdrop\n```\n\n```bash\nservice apache2 reload\n```\n\n<br>\n\n## Coturn and PairDrop via Docker Compose\n\n### Setup container\nTo run coturn and PairDrop at once by using the `docker-compose-coturn.yml` with TURN over TLS enabled\nyou need to follow these steps:\n\n1. Generate or retrieve certificates for your `<DOMAIN>` (e.g. letsencrypt / certbot)\n2. Create `./ssl` folder: `mkdir ssl`\n3. Copy your ssl-certificates and the privkey to `./ssl` \n4. Restrict access to `./ssl`: `chown -R nobody:nogroup ./ssl`\n5. Create a dh-params file: `openssl dhparam -out ./ssl/dhparams.pem 4096` \n6. Copy `rtc_config_example.json` to `rtc_config.json`\n7. Copy `turnserver_example.conf` to `turnserver.conf`\n8. Change `<DOMAIN>` in both files to the domain where your PairDrop instance is running \n9. Change `username` and `password` in `turnserver.conf` and `rtc-config.json`\n10. To start the container including coturn run: \\\n  `docker compose -f docker-compose-coturn.yml up -d`\n\n<br>\n\n#### Setup container\nTo restart the container including coturn run: \\\n  `docker compose -f docker-compose-coturn.yml restart`\n\n<br>\n\n#### Setup container\nTo stop the container including coturn run: \\\n  `docker compose -f docker-compose-coturn.yml stop`\n\n<br>\n\n### Firewall\nTo run PairDrop including its own coturn-server you need to punch holes in the firewall. These ports must be opened additionally:\n- 3478 tcp/udp\n- 5349 tcp/udp\n- 10000:20000 tcp/udp\n\n<br>\n\n## Local Development\n\n### Install\n\nAll files needed for developing are available in the folder `./dev`.\n\nFor convenience, there is also a docker compose file for developing:\n\n#### Developing with docker compose\nFirst, [Install docker with docker compose.](https://docs.docker.com/compose/install/)\n\nThen, clone the repository and run docker compose:\n\n```bash\ngit clone https://github.com/schlagmichdoch/PairDrop.git && cd PairDrop\n```\n```bash\ndocker compose -f docker-compose-dev.yml up --no-deps --build\n```\n\nNow point your web browser to `http://localhost:8080`.\n\n- To debug the Node.js server, run `docker logs pairdrop`.\n- After changes to the code you have to rerun the `docker compose` command\n\n<br>\n\n#### Testing PWA related features\n\nPWAs requires the app to be served under a correctly set up and trusted TLS endpoint.\n\nThe NGINX container creates a CA certificate and a website certificate for you. \nTo correctly set the common name of the certificate, \nyou need to change the FQDN environment variable in `docker-compose-dev.yml`\nto the fully qualified domain name of your workstation. (Default: localhost)\n\nIf you want to test PWA features, you need to trust the CA of the certificate for your local deployment. \\\nFor your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. \\\nInstall that certificate to the trust store of your operating system. \\\n\n##### Windows\n- Make sure to install it to the `Trusted Root Certification Authorities` store.\n\n##### macOS\n- Double-click the installed CA certificate in `Keychain Access`,\n- expand `Trust`, and select `Always Trust` for SSL.\n\n##### Firefox\nFirefox uses its own trust store. To install the CA:\n- point Firefox at `http://<Your FQDN>:8080/ca.crt` (Default: `http://localhost:8080/ca.crt`)\n- When prompted, select `Trust this CA to identify websites` and click _OK_.\n\nAlternatively:\n1. Download `ca.crt` from `http://<Your FQDN>:8080/ca.crt` (Default: `http://localhost:8080/ca.crt`)\n2. Go to `about:preferences#privacy` scroll down to `Security` and `Certificates` and click `View Certificates`\n3. Import the downloaded certificate file (step 1)\n\n##### Chrome\n- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`).\n- Additionally, after installing a new cert, you need to clear the Storage (DevTools → Application → Clear storage → Clear site data).\n\n##### Google Chrome\n- To skip the installation of the certificate, you can also open `chrome://flags/#unsafely-treat-insecure-origin-as-secure`\n- The feature `Insecure origins treated as secure` must be enabled and the list must include your PairDrop test instance. E.g.: `http://127.0.0.1:3000,https://127.0.0.1:8443`\n\nPlease note that the certificates (CA and webserver cert) expire after a day.\nAlso, whenever you restart the NGINX Docker container new certificates are created.\n\nThe site is served on `https://<Your FQDN>:8443` (Default: `https://localhost:8443`).\n\n[< Back](/README.md)\n"
  },
  {
    "path": "docs/how-to.md",
    "content": "# How-To\n\n## Send directly from share menu on iOS\nI created an iOS shortcut to send images, files, folder, URLs \\\nor text directly from the share-menu \nhttps://routinehub.co/shortcut/13990/\n\n[//]: # (Todo: Add screenshots)\n\n<br>\n\n## Send directly from share menu on Android\nThe [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented.\n\nWhen the PWA is installed, it will register itself to the share-menu of the device automatically.\n\n<br>\n\n## Send directly via command-line interface\nSend files or text with PairDrop via command-line interface. \\\nThis opens PairDrop in the default browser where you can choose the receiver.\n\n### Usage\n```bash\npairdrop -h\n```\n```\nSend files or text with PairDrop via command-line interface.\nCurrent domain: https://pairdrop-dev.onrender.com/\n\nUsage:\nOpen PairDrop:\t\tpairdrop\nSend files:\t\tpairdrop file1/directory1 (file2/directory2 file3/directory3 ...)\nSend text:\t\tpairdrop -t \"text\"\nSpecify domain:\t\tpairdrop -d \"https://pairdrop.net/\"\nShow this help text:\tpairdrop (-h|--help)\n\nThis pairdrop-cli version was released alongside v1.10.4\n```\n\n<br>\n\n### Setup\n\n#### Linux / Mac\n1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases)\n   ```shell\n   wget \"https://github.com/schlagmichdoch/PairDrop/releases/download/v1.11.2/pairdrop-cli.zip\"\n   ```\n   or\n   ```shell\n   curl -LO \"https://github.com/schlagmichdoch/PairDrop/releases/download/v1.11.2/pairdrop-cli.zip\"\n   ```\n2. Unzip the archive to a folder of your choice e.g. `/usr/share/pairdrop-cli/`\n   ```shell\n   sudo unzip pairdrop-cli.zip -d /usr/share/pairdrop-cli/\n   ```\n3. Copy the file _.pairdrop-cli-config.example_ to _.pairdrop-cli-config_\n   ```shell\n   sudo cp /usr/share/pairdrop-cli/.pairdrop-cli-config.example /usr/share/pairdrop-cli/.pairdrop-cli-config\n   ```\n4. Make the bash file _pairdrop_ executable\n   ```shell\n   sudo chmod +x /usr/share/pairdrop-cli/pairdrop\n   ```\n5. Add a symlink to /usr/local/bin/ to include _pairdrop_ to _PATH_\n   ```shell\n   sudo ln -s /usr/share/pairdrop-cli/pairdrop /usr/local/bin/pairdrop\n   ```\n\n<br>\n\n#### Windows\n1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases)\n2. Put file in a preferred folder e.g. `C:\\Program Files\\pairdrop-cli`\n3. Inside this folder, copy the file _.pairdrop-cli-config.example_ to _.pairdrop-cli-config_\n4. Search for and open `Edit environment variables for your account`\n5. Click `Environment Variables…`\n6. Under _System Variables_ select `Path` and click _Edit..._\n7. Click _New_, insert the preferred folder (`C:\\Program Files\\pairdrop-cli`), click *OK* until all windows are closed\n8. Reopen Command prompt window\n\n**Requirements**\n\nAs Windows cannot execute bash scripts natively, you need to install [Git Bash](https://gitforwindows.org/).\n\nThen, you can also use pairdrop-cli from the default Windows Command Prompt \nby using the shell file instead of the bash file which then itself executes \n_pairdrop-cli_ (the bash file) via the Git Bash.\n```shell\npairdrop.sh -h\n```\n\n<br>\n\n## Send multiple files and directories directly from context menu on Windows\n\n### Registering to open files with PairDrop\nIt is possible to send multiple files with PairDrop via the context menu by adding pairdrop-cli to Windows `Send to` menu:\n1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases)\n2. Unzip the archive to a folder of your choice e.g. `C:\\Program Files\\pairdrop-cli\\`\n3. Inside this folder, copy the file _.pairdrop-cli-config.example_ to _.pairdrop-cli-config_\n4. Copy the shortcut _send with PairDrop.lnk_\n5. Hit Windows Key+R, type: `shell:sendto` and hit Enter.\n6. Paste the copied shortcut into the directory\n7. Open the properties window of the shortcut and edit the link field to point to _send-with-pairdrop.ps1_ located in the folder you used in step 2: \\\n   `\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\" -File \"C:\\Program Files\\pairdrop-cli\\send-with-pairdrop.ps1\"`\n8. You are done! You can now send multiple files and directories directly via PairDrop:\n\n   _context menu_ > _Send to_ > _PairDrop_\n\n##### Requirements\nAs Windows cannot execute bash scripts natively, you need to install [Git Bash](https://gitforwindows.org/).\n\n<br>\n\n## Send multiple files and directories directly from context menu on Ubuntu using Nautilus\n\n### Registering to open files with PairDrop\nIt is possible to send multiple files with PairDrop via the context menu by adding pairdrop-cli to Nautilus `Scripts` menu:\n1. Register _pairdrop_ as executable via [guide above](#linux).\n2. Copy the shell file _send-with-pairdrop_ to `~/.local/share/nautilus/scripts/` to include it in the context menu\n   ```shell\n   cp /usr/share/pairdrop-cli/send-with-pairdrop ~/.local/share/nautilus/scripts/\n   ```\n3. Make the shell file _send-with-pairdrop_ executable\n   ```shell\n   chmod +x ~/.local/share/nautilus/scripts/send-with-pairdrop\n   ```\n4. You are done! You can now send multiple files and directories directly via PairDrop:\n\n   _context menu_ > _Scripts_ > _send-with-pairdrop_\n\n<br>\n\n## File Handling API\nThe [File Handling API](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files)\nwas implemented, but it was removed as default file associations were overwritten ([#17](https://github.com/schlagmichdoch/PairDrop/issues/17),\n[#116](https://github.com/schlagmichdoch/PairDrop/issues/116) [#190](https://github.com/schlagmichdoch/PairDrop/issues/190))\nand it only worked with explicitly specified file types and couldn't handle directories at all.\n\n[< Back](/README.md)\n"
  },
  {
    "path": "docs/technical-documentation.md",
    "content": "# Technical Documentation\n## Encryption, WebRTC, STUN and TURN\n\nEncryption is mandatory for WebRTC connections and completely done by the browser itself.\n\nWhen the peers are first connecting, \\\na channel is created by exchanging their signaling info. \\\nThis signaling information includes some sort of public key \\\nand is specific to the clients IP address. \\\nThat is what the STUN Server is used for: \\\nit simply returns your public IP address \\\nas you only know your local ip address \\\nif behind a NAT (router).\n\nThe transfer of the signaling info is done by the \\\nPairDrop / Snapdrop server using secure websockets. \\\nAfter that the channel itself is completely peer-to-peer \\\nand all info can only be decrypted by the receiver. \\\nWhen the two peers are on the same network \\\nor when they are not behind any NAT system \\\n(which they are always for classic \\\nSnapdrop and for not paired users on PairDrop) \\\nthe files are send directly peer-to-peer.\n\nWhen a user is behind a NAT (behind a router) \\\nthe contents are channeled through a TURN server. \\\nBut again, the contents send via the channel \\\ncan only be decrypted by the receiver. \\\nSo a rogue TURN server could only \\\nsee that there is a connection, but not what is sent. \\\nObviously, connections which are channeled through a TURN server \\\nare not as fast as peer-to-peer.\n\nThe selection whether a TURN server is needed \\\nor not is also done automatically by the web browser. \\\nIt simply iterated through the configured \\\nRTC iceServers and checks what works. \\\nOnly if the STUN server is not sufficient, \\\nthe TURN server is used.\n\n![img](https://www.wowza.com/wp-content/uploads/WeRTC-Encryption-Diagrams-01.jpg)\n_Diagram created by wowza.com_\n\nGood thing: if your device has an IPv6 address \\\nit is uniquely reachable by that address. \\\nAs I understand it, when both devices are using \\\nIPv6 addresses there is no need for a TURN server in any scenario.\n\nLearn more by reading https://www.wowza.com/blog/webrtc-encryption-and-security \\\nwhich gives a good insight into STUN, TURN and WebRTC.\n\n\n## Device Pairing\n\nThe pairing functionality uses the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).\n\nIt works by creating long secrets that are served \\\nby the server to the initiating and requesting pair peer, \\\nwhen the inserted key is correct. \\\nThese long secrets are then saved to an \\\nindexedDB database in the web browser. \\\nIndexedDB is somewhat the successor of localStorage \\\nas saved data is shared between all tabs. \\\nIt goes one step further by making the data persistent \\\nand available offline if implemented to a PWA.\n\nAll secrets a client has saved to its database \\\nare sent to the PairDrop server. \\\nPeers with a common secret are discoverable \\\nto each other analog to peers with the same \\\nIP address are discoverable by each other.\n\nWhat I really like about this approach (and the reason I implemented it) \\\nis that devices on the same network are always \\\nvisible regardless whether any devices are paired or not. \\\nThe main user flow is never obstructed. \\\nPaired devices are simply shown additionally. \\\nThis makes it in my idea better than the idea of \\\nusing a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214).\n\n\n[< Back](/README.md)\n"
  },
  {
    "path": "licenses/BSD_3-Clause-zip-js",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2023, Gildas Lormeau\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
  },
  {
    "path": "licenses/MIT-NoSleep",
    "content": "The MIT License (MIT)\n\nCopyright (c) Rich Tibbett\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "licenses/MIT-heic2any",
    "content": "MIT License\n\nCopyright (c) 2020 Alex Corvi\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"pairdrop\",\n  \"version\": \"1.11.2\",\n  \"type\": \"module\",\n  \"description\": \"\",\n  \"main\": \"server/index.js\",\n  \"scripts\": {\n    \"start\": \"node server/index.js\",\n    \"start:prod\": \"node server/index.js --rate-limit --auto-restart\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"express\": \"^4.18.2\",\n    \"express-rate-limit\": \"^7.1.5\",\n    \"ua-parser-js\": \"^1.0.37\",\n    \"unique-names-generator\": \"^4.3.0\",\n    \"ws\": \"^8.16.0\"\n  },\n  \"engines\": {\n    \"node\": \">=15\"\n  }\n}\n"
  },
  {
    "path": "pairdrop-cli/.gitignore",
    "content": ".pairdrop-cli-config"
  },
  {
    "path": "pairdrop-cli/.pairdrop-cli-config.example",
    "content": "DOMAIN=https://pairdrop.net/"
  },
  {
    "path": "pairdrop-cli/pairdrop",
    "content": "#!/bin/bash\nset -e\n\n# PairDrop version when this file was last changed\nversion=\"v1.10.4\"\n\n############################################################\n# Help                                                     #\n############################################################\nhelp()\n{\n   # Display Help\n   echo \"Send files or text with PairDrop via command-line interface.\"\n   echo \"Current domain: ${DOMAIN}\"\n   echo\n   echo \"Usage:\"\n   echo -e \"Open PairDrop:\\t\\t$(basename \"$0\")\"\n   echo -e \"Send files:\\t\\t$(basename \"$0\") file1/directory1 (file2/directory2 file3/directory3 ...)\"\n   echo -e \"Send text:\\t\\t$(basename \"$0\") -t \\\"text\\\"\"\n   echo -e \"Specify domain:\\t\\t$(basename \"$0\") -d \\\"https://pairdrop.net/\\\"\"\n   echo -e \"Show this help text:\\t$(basename \"$0\") (-h|--help)\"\n   echo\n   echo \"This pairdrop-cli version was released alongside ${version}\"\n}\n\nopenPairDrop()\n{\n  url=\"$DOMAIN\"\n  if [[ -n $params ]];then\n    url=\"${url}?${params}\"\n  fi\n  if [[ -n $hash ]];then\n    url=\"${url}#${hash}\"\n  fi\n\n  echo \"PairDrop is opening at $DOMAIN\"\n  if [[ $OS == \"Windows\" ]];then\n    start \"$url\"\n  elif [[ $OS == \"Mac\" ]];then\n    open \"$url\"\n  elif [[ $OS == \"WSL\" || $OS == \"WSL2\" ]];then\n    powershell.exe /c \"Start-Process ${url}\"\n  else\n    xdg-open \"$url\" > /dev/null 2>&1\n  fi\n\n\n  exit\n\n}\n\nsetOs()\n{\n  unameOut=$(uname -a)\n  case \"${unameOut}\" in\n      *Microsoft*)     OS=\"WSL\";; #must be first since Windows subsystem for linux will have Linux in the name too\n      *microsoft*)     OS=\"WSL2\";; #WARNING: My v2 uses ubuntu 20.4 at the moment slightly different name may not always work\n      Linux*)          OS=\"Linux\";;\n      Darwin*)         OS=\"Mac\";;\n      CYGWIN*)         OS=\"Cygwin\";;\n      MINGW*)          OS=\"Windows\";;\n      *Msys)           OS=\"Windows\";;\n      *)               OS=\"UNKNOWN:${unameOut}\"\n  esac\n}\n\nspecifyDomain()\n{\n  [[ ! $1 = http* ]] || [[ ! $1 = */ ]] && echo \"Incorrect format. Specify domain like https://pairdrop.net/\" && exit\n  echo \"DOMAIN=${1}\" > \"$config_path\"\n  echo -e \"Domain is now set to:\\n$1\\n\"\n}\n\nsendText()\n{\n    params=\"base64text=hash\"\n    hash=$(echo -n \"${OPTARG}\" | base64)\n\n    if [[ $(echo -n \"$hash\" | wc -m) -gt 32600 ]];then\n      params=\"base64text=paste\"\n      if [[ $OS == \"Windows\" || $OS == \"WSL\" || $OS == \"WSL2\" ]];then\n        echo -n \"$hash\" | clip.exe\n      elif [[ $OS == \"Mac\" ]];then\n        echo -n \"$hash\" | pbcopy\n      else\n        (echo -n \"$hash\" | xclip) || echo \"You need to install xclip for sending bigger files from cli\"\n      fi\n      hash=\n    fi\n\n    openPairDrop\n    exit\n}\n\nescapePSPath()\n{\n  local path=$1\n  \n  # escape '[' and ']' with grave accent (`) character\n  pathPS=${path//[/\\`[}\n  pathPS=${pathPS//]/\\`]}\n  # escape single quote (') with another single quote (')\n  pathPS=${pathPS//\\'/\\'\\'}\n  \n  # Convert GitHub bash path \"/i/path\" to Windows path \"I:/path\"\n  if [[ $pathPS == /* ]]; then\n    # Remove preceding slash\n    pathPS=\"${pathPS#/}\"\n    # Convert drive letter to uppercase\n    driveLetter=$(echo \"${pathPS::1}\" | tr '[:lower:]' '[:upper:]')\n    # Put together absolute path as used in Windows\n    pathPS=\"${driveLetter}:${pathPS:1}\"        \n  fi\n  \n  echo \"$pathPS\"\n}\n\nsendFiles()\n{\n  params=\"base64zip=hash\"\n  workingDir=\"$(pwd)\"\n  tmpDir=\"/tmp/pairdrop-cli-temp/\"\n  tmpDirPS=\"\\$env:TEMP/pairdrop-cli-temp/\"\n  \n  index=0\n  directoryBaseNamesUnix=()\n  directoryPathsUnix=()\n  filePathsUnix=()\n  directoryCount=0\n  fileCount=0\n  pathsPS=\"\"\n\n  #create tmp folder if it does not exist already\n  if [[ ! -d \"$tmpDir\" ]]; then\n    mkdir \"$tmpDir\"\n  fi\n\n  for arg in \"$@\"; do\n   echo \"$arg\"\n   [[ ! -e \"$arg\" ]] && echo \"The given path $arg does not exist.\" && exit\n\n    # Remove trailing slash from directory\n    arg=\"${arg%/}\"\n\n    # get absolute path and basename of file/directory\n    absolutePath=$(realpath \"$arg\")\n    baseName=$(basename \"$absolutePath\")\n    directoryPath=$(dirname \"$absolutePath\")\n\n    if [[ -d $absolutePath ]]; then\n      # is directory\n      ((directoryCount+=1))\n      # add basename and directory path to arrays\n      directoryBaseNamesUnix+=(\"$baseName\")\n      directoryPathsUnix+=(\"$directoryPath\")\n    else\n      # is file\n      ((fileCount+=1))\n      absolutePathUnix=$absolutePath\n      # append new path and separate paths with space\n      filePathsUnix+=(\"$absolutePathUnix\")\n    fi\n\n    # Prepare paths for PowerShell on Windows\n    if [[ $OS == \"Windows\" ]];then\n      absolutePathPS=$(escapePSPath \"$absolutePath\")\n      \n      # append new path and separate paths with commas\n      pathsPS+=\"'${absolutePathPS}', \"\n    fi\n    \n    # set fileNames on first loop\n    if [[ $index == 0 ]]; then\n      baseNameU=${baseName// /_}\n\n      # Prevent baseNameU being empty for hidden files by removing the preceding dot\n      if [[ $baseNameU == .* ]]; then\n        baseNameU=${baseNameU#.*}\n      fi\n\n      # only use trunk of basename \"document.txt\" -> \"document\"\n      baseNameTrunk=${baseNameU%.*}\n      \n      # remove all special characters\n      zipName=${baseNameTrunk//[^a-zA-Z0-9_]/}\n      \n      zipToSendAbs=\"${tmpDir}${zipName}_pairdrop.zip\"\n      wrapperZipAbs=\"${tmpDir}${zipName}_pairdrop_wrapper.zip\"\n\n      if [[ $OS == \"Windows\" ]];then\n        zipToSendAbsPS=\"${tmpDirPS}${zipName}_pairdrop.zip\"\n        wrapperZipAbsPS=\"${tmpDirPS}${zipName}_pairdrop_wrapper.zip\"\n      fi\n    fi\n\n    ((index+=1)) # somehow ((index++)) stops the script\n  done\n\n  # Prepare paths for PowerShell on Windows\n  if [[ $OS == \"Windows\" ]];then\n    # remove trailing comma\n    pathsPS=${pathsPS%??}\n  fi\n\n  echo \"Preparing ${fileCount} files and ${directoryCount} directories...\"\n\n  # if arguments include files only -> zip files once so files it is unzipped by sending browser\n  # if arguments include directories -> wrap first zip in a second wrapper zip so that after unzip by sending browser a zip file is sent to receiver\n  #\n  # Preferred zip structure:\n  # pairdrop \"d1/d2/d3/f1\" \"../../d4/d5/d6/f2\" \"d7/\" \"../d8/\" \"f5\"\n  # zip structure: pairdrop.zip\n  #                |-f1\n  #                |-f2\n  #                |-d7/\n  #                |-d8/\n  #                |-f5\n  # -> truncate (relative) paths but keep directories\n  \n  [[ -e \"$zipToSendAbs\" ]] && echo \"Cannot overwrite $zipToSendAbs. Please remove first.\" && exit\n\n  if [[ $OS == \"Windows\" ]];then\n    # Powershell does preferred zip structure natively\n    powershell.exe -Command \"Compress-Archive -Path ${pathsPS} -DestinationPath ${zipToSendAbsPS}\"\n  else\n    # Workaround needed to create preferred zip structure on unix systems\n    # Create zip file with all single files by junking the path\n    if [[ $fileCount != 0 ]]; then\n      zip -q -b /tmp/ -j -0 -r \"$zipToSendAbs\" \"${filePathsUnix[@]}\"\n    fi\n\n    # Add directories recursively to zip file\n    index=0\n    while [[ $index < $directoryCount ]]; do\n      # workaround to keep directory name but junk the rest of the paths\n\n      # cd to path above directory\n      cd \"${directoryPathsUnix[index]}\"\n\n      # add directory to zip without junking the path\n      zip -q -b /tmp/ -0 -u -r \"$zipToSendAbs\" \"${directoryBaseNamesUnix[index]}\"\n\n      # cd back to working directory\n      cd \"$workingDir\"\n\n      ((index+=1)) # somehow ((index++)) stops the script\n    done\n  fi\n\n  # If directories are included send as zip\n  # -> Create additional zip wrapper which will be unzipped by the sending browser\n  if [[ \"$directoryCount\" != 0 ]]; then\n    echo \"Bundle as ZIP file...\"\n\n    # Prevent filename from being absolute zip path by \"cd\"ing to directory before zipping\n    zipToSendDirectory=$(dirname \"$zipToSendAbs\")\n    zipToSendBaseName=$(basename \"$zipToSendAbs\")\n\n    cd \"$zipToSendDirectory\"\n    \n    [[ -e \"$wrapperZipAbs\" ]] && echo \"Cannot overwrite $wrapperZipAbs. Please remove first.\" && exit\n    \n    if [[ $OS == \"Windows\" ]];then\n      powershell.exe -Command \"Compress-Archive -Path ${zipToSendBaseName} -DestinationPath ${wrapperZipAbsPS} -CompressionLevel Optimal\"\n    else\n      zip -q -b /tmp/ -0 \"$wrapperZipAbs\" \"$zipToSendBaseName\"\n    fi\n    cd \"$workingDir\"\n\n    # remove inner zip file and set wrapper as zipToSend (do not differentiate between OS as this is done via Git Bash on Windows)\n    rm \"$zipToSendAbs\"\n\n    zipToSendAbs=$wrapperZipAbs\n  fi\n\n  # base64 encode zip file\n  if [[ $OS == \"Mac\" ]];then\n    hash=$(base64 -i \"$zipToSendAbs\")\n  else\n    hash=$(base64 -w 0 \"$zipToSendAbs\")\n  fi\n\n  # remove zip file (do not differentiate between OS as this is done via Git Bash on Windows)\n  rm \"$zipToSendAbs\"\n\n  if [[ $(echo -n \"$hash\" | wc -m) -gt 1000 ]];then\n    params=\"base64zip=paste\"\n\n    # Copy $hash to clipboard\n    if [[ $OS == \"Windows\" || $OS == \"WSL\" || $OS == \"WSL2\" ]];then\n      echo -n \"$hash\" | clip.exe\n    elif [[ $OS == \"Mac\" ]];then\n      echo -n \"$hash\" | pbcopy\n    elif [ -n \"$WAYLAND_DISPLAY\" ]; then\n      # Wayland\n      if ! command -v wl-copy &> /dev/null; then\n          echo -e \"You need to install 'wl-copy' to send bigger filePathsUnix from cli\"\n          echo \"Try: sudo apt install wl-clipboard\"\n          exit 1\n      fi\n      # Workaround to prevent use of Pipe which has a max letter limit\n      echo -n \"$hash\" > /tmp/pairdrop-cli-temp/pairdrop_hash_temp\n      wl-copy < /tmp/pairdrop-cli-temp/pairdrop_hash_temp\n      rm /tmp/pairdrop-cli-temp/pairdrop_hash_temp\n    else\n      # X11\n      if ! command -v xclip &> /dev/null; then\n          echo -e \"You need to install 'xclip' to send bigger filePathsUnix from cli\"\n          echo \"Try: sudo apt install xclip\"\n          exit 1\n      fi\n      echo -n \"$hash\" | xclip -sel c\n    fi\n    hash=\n  fi\n\n  openPairDrop\n  exit\n}\n\n############################################################\n############################################################\n# Main program                                             #\n############################################################\n############################################################\nscript_path=\"$( cd -- \"$(dirname \"$0\")\" >/dev/null 2>&1 ; pwd -P )\"\n\npushd . > '/dev/null';\nscript_path=\"${BASH_SOURCE[0]:-$0}\";\n\nwhile [ -h \"$script_path\" ];\ndo\n  cd \"$( dirname -- \"$script_path\"; )\";\n  script_path=\"$( readlink -f -- \"$script_path\"; )\";\ndone\n\ncd \"$( dirname -- \"$script_path\"; )\" > '/dev/null';\nscript_path=\"$( pwd; )\";\npopd  > '/dev/null';\n\nconfig_path=\"${script_path}/.pairdrop-cli-config\"\n\n# If config file does not exist, try to create it. If it fails log error message and exit\n[ ! -f \"$config_path\" ] &&\n  specifyDomain \"https://pairdrop.net/\" &&\n  [ ! -f \"$config_path\" ] &&\n  echo \"Could not create config file. Add 'DOMAIN=https://pairdrop.net/' to a file called .pairdrop-cli-config in the same file as this 'pairdrop' bash file (${script_path})\" &&\n  exit\n\n# Read config variables\nexport \"$(grep -v '^#' \"$config_path\" | xargs)\"\n\nsetOs\n\n############################################################\n# Process the input options. Add options as needed.        #\n############################################################\n# Get the options\n# open PairDrop if no options are given\n[[ $# -eq 0 ]] && openPairDrop && exit\n\n#  display help and exit if first argument is \"--help\" or more than 2 arguments are given\n[ \"$1\" == \"--help\" ] && help && exit\n\nwhile getopts \"d:ht:*\" option; do\n  case $option in\n    d) # specify domain - show help and exit if too many arguments\n      [[ $# -gt 2 ]] && help && exit\n      specifyDomain \"$2\"\n      exit;;\n    t) # Send text - show help and exit if too many arguments\n      [[ $# -gt 2 ]] && help && exit\n      sendText\n      exit;;\n    h | ?) # display help and exit\n      help\n      exit;;\n    esac\ndone\n\n# Send file(s)\nsendFiles \"$@\"\n"
  },
  {
    "path": "pairdrop-cli/pairdrop.sh",
    "content": "#!/bin/bash\nparent_path=$( cd \"$(dirname \"${BASH_SOURCE[0]}\")\" || exit ; pwd -P )\n\ncd \"$parent_path\" || exit\n\n./pairdrop \"$@\""
  },
  {
    "path": "pairdrop-cli/send-with-pairdrop",
    "content": "#!/bin/bash\n\n# Initialize an array\nlines=()\n\n# Read each line into the array\nwhile IFS= read -r line; do\n    lines+=(\"$line\")\ndone <<< \"$NAUTILUS_SCRIPT_SELECTED_FILE_PATHS\"\n\n# Get the length of the array\nlength=${#lines[@]}\n\n# Remove the last entry\nunset 'lines[length-1]'\n\npairdrop \"${lines[@]}\""
  },
  {
    "path": "pairdrop-cli/send-with-pairdrop.ps1",
    "content": "$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\n& \"$scriptDir\\pairdrop.sh\" $args"
  },
  {
    "path": "public/fonts/OpenSans/OFL.txt",
    "content": "Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "public/fonts/OpenSans/README.txt",
    "content": "Open Sans Variable Font\n=======================\n\nThis download contains Open Sans as both variable fonts and static fonts.\n\nOpen Sans is a variable font with these axes:\n  wdth\n  wght\n\nThis means all the styles are contained in these files:\n  OpenSans-VariableFont_wdth,wght.ttf\n  OpenSans-Italic-VariableFont_wdth,wght.ttf\n\nIf your app fully supports variable fonts, you can now pick intermediate styles\nthat aren’t available as static fonts. Not all apps support variable fonts, and\nin those cases you can use the static font files for Open Sans:\n  static/OpenSans_Condensed-Light.ttf\n  static/OpenSans_Condensed-Regular.ttf\n  static/OpenSans_Condensed-Medium.ttf\n  static/OpenSans_Condensed-SemiBold.ttf\n  static/OpenSans_Condensed-Bold.ttf\n  static/OpenSans_Condensed-ExtraBold.ttf\n  static/OpenSans_SemiCondensed-Light.ttf\n  static/OpenSans_SemiCondensed-Regular.ttf\n  static/OpenSans_SemiCondensed-Medium.ttf\n  static/OpenSans_SemiCondensed-SemiBold.ttf\n  static/OpenSans_SemiCondensed-Bold.ttf\n  static/OpenSans_SemiCondensed-ExtraBold.ttf\n  static/OpenSans-Light.ttf\n  static/OpenSans-Regular.ttf\n  static/OpenSans-Medium.ttf\n  static/OpenSans-SemiBold.ttf\n  static/OpenSans-Bold.ttf\n  static/OpenSans-ExtraBold.ttf\n  static/OpenSans_Condensed-LightItalic.ttf\n  static/OpenSans_Condensed-Italic.ttf\n  static/OpenSans_Condensed-MediumItalic.ttf\n  static/OpenSans_Condensed-SemiBoldItalic.ttf\n  static/OpenSans_Condensed-BoldItalic.ttf\n  static/OpenSans_Condensed-ExtraBoldItalic.ttf\n  static/OpenSans_SemiCondensed-LightItalic.ttf\n  static/OpenSans_SemiCondensed-Italic.ttf\n  static/OpenSans_SemiCondensed-MediumItalic.ttf\n  static/OpenSans_SemiCondensed-SemiBoldItalic.ttf\n  static/OpenSans_SemiCondensed-BoldItalic.ttf\n  static/OpenSans_SemiCondensed-ExtraBoldItalic.ttf\n  static/OpenSans-LightItalic.ttf\n  static/OpenSans-Italic.ttf\n  static/OpenSans-MediumItalic.ttf\n  static/OpenSans-SemiBoldItalic.ttf\n  static/OpenSans-BoldItalic.ttf\n  static/OpenSans-ExtraBoldItalic.ttf\n\nGet started\n-----------\n\n1. Install the font files you want to use\n\n2. Use your app's font picker to view the font family and all the\navailable styles\n\nLearn more about variable fonts\n-------------------------------\n\n  https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts\n  https://variablefonts.typenetwork.com\n  https://medium.com/variable-fonts\n\nIn desktop apps\n\n  https://theblog.adobe.com/can-variable-fonts-illustrator-cc\n  https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts\n\nOnline\n\n  https://developers.google.com/fonts/docs/getting_started\n  https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide\n  https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts\n\nInstalling fonts\n\n  MacOS: https://support.apple.com/en-us/HT201749\n  Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux\n  Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows\n\nAndroid Apps\n\n  https://developers.google.com/fonts/docs/android\n  https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts\n\nLicense\n-------\nPlease read the full license text (OFL.txt) to understand the permissions,\nrestrictions and requirements for usage, redistribution, and modification.\n\nYou can use them in your products & projects – print or digital,\ncommercial or otherwise.\n\nThis isn't legal advice, please consider consulting a lawyer and see the full\nlicense for all details.\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n    <!-- Web App Config -->\n    <title>PairDrop | Transfer Files Cross-Platform. No Setup, No Signup.</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <meta name=\"theme-color\" content=\"#3367d6\">\n    <meta name=\"color-scheme\" content=\"dark light\">\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n    <meta name=\"apple-mobile-web-app-title\" content=\"PairDrop\">\n    <meta name=\"application-name\" content=\"PairDrop\">\n    <!-- Descriptions -->\n    <meta name=\"description\" content=\"Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.\">\n    <meta name=\"keywords\" content=\"File, Transfer, Share, Peer2Peer\">\n    <meta name=\"author\" content=\"schlagmichdoch\">\n    <meta property=\"og:title\" content=\"PairDrop\">\n    <meta property=\"og:type\" content=\"article\">\n    <meta property=\"og:url\" content=\"https://pairdrop.net/\">\n    <meta property=\"og:author\" content=\"https://github.com/schlagmichdoch\">\n    <meta name=\"twitter:author\" content=\"@schlagmichdoch\">\n    <meta name=\"twitter:card\" content=\"summary_large_image\">\n    <meta name=\"twitter:description\" content=\"Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.\">\n    <meta name=\"og:description\" content=\"Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.\">\n    <!-- Icons -->\n    <link rel=\"icon\" sizes=\"96x96\" href=\"images/favicon-96x96.png\">\n    <link rel=\"shortcut icon\" href=\"images/favicon-96x96.png\">\n    <link rel=\"apple-touch-icon\" href=\"images/apple-touch-icon.png\">\n    <link rel=\"apple-touch-icon-precomposed\" href=\"images/apple-touch-icon.png\">\n    <meta name=\"msapplication-TileImage\" content=\"images/mstile-150x150.png\">\n    <link rel=\"fluid-icon\" type=\"image/png\" href=\"images/android-chrome-192x192.png\">\n    <meta name=\"twitter:image\" content=\"images/logo_transparent_512x512.png\">\n    <meta property=\"og:image\" content=\"images/logo_transparent_512x512.png\">\n    <!-- Resources -->\n    <link rel=\"preload\" href=\"lang/en.json\" as=\"fetch\">\n    <link rel=\"preload\" href=\"fonts/OpenSans/static/OpenSans-Medium.ttf\" as=\"font\" type=\"font/ttf\" crossorigin>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"styles/styles-main.css\">\n    <link rel=\"manifest\" href=\"manifest.json\">\n</head>\n\n<body translate=\"no\">\n    <header class=\"row-reverse wrap opacity-0\">\n        <a href=\"#about\" class=\"icon-button\" data-i18n-key=\"header.about\" data-i18n-attrs=\"title aria-label\">\n            <svg class=\"icon\">\n                <use xlink:href=\"#info-outline\"></use>\n            </svg>\n        </a>\n        <div id=\"language-selector\" class=\"icon-button\" data-i18n-key=\"header.language-selector\" data-i18n-attrs=\"title\">\n            <svg class=\"icon\">\n                <use xlink:href=\"#icon-language-selector\"></use>\n            </svg>\n        </div>\n        <div id=\"theme-wrapper\">\n            <div id=\"theme-auto\" class=\"icon-button selected\" data-i18n-key=\"header.theme-auto\" data-i18n-attrs=\"title\">\n                <svg class=\"icon\">\n                    <use xlink:href=\"#icon-theme-auto\"></use>\n                </svg>\n            </div>\n            <div>\n                <div id=\"theme-light\" class=\"icon-button\" data-i18n-key=\"header.theme-light\" data-i18n-attrs=\"title\">\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#icon-theme-light\"></use>\n                    </svg>\n                </div>\n                <div id=\"theme-dark\" class=\"icon-button\" data-i18n-key=\"header.theme-dark\" data-i18n-attrs=\"title\">\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#icon-theme-dark\"></use>\n                    </svg>\n                </div>\n            </div>\n        </div>\n        <div id=\"notification\" class=\"icon-button\" data-i18n-key=\"header.notification\" data-i18n-attrs=\"title\" hidden>\n            <svg class=\"icon\">\n                <use xlink:href=\"#notifications\"></use>\n            </svg>\n        </div>\n        <div id=\"install\" class=\"icon-button\" data-i18n-key=\"header.install\" data-i18n-attrs=\"title\" hidden>\n            <svg class=\"icon\">\n                <use xlink:href=\"#homescreen\"></use>\n            </svg>\n        </div>\n        <div id=\"pair-device\" class=\"icon-button\" data-i18n-key=\"header.pair-device\" data-i18n-attrs=\"title\">\n            <svg class=\"icon\">\n                <use xlink:href=\"#pair-device-icon\"></use>\n            </svg>\n        </div>\n        <div id=\"edit-paired-devices\" class=\"icon-button\" data-i18n-key=\"header.edit-paired-devices\" data-i18n-attrs=\"title\" hidden>\n            <svg class=\"icon\">\n                <use xlink:href=\"#edit-pair-devices-icon\"></use>\n            </svg>\n        </div>\n        <div id=\"join-public-room\" class=\"icon-button\" data-i18n-key=\"header.join-public-room\" data-i18n-attrs=\"title\">\n            <svg class=\"icon\">\n                <use xlink:href=\"#public-room-icon\"></use>\n            </svg>\n        </div>\n        <div id=\"expand\" class=\"icon-button\" data-i18n-key=\"header.expand\" data-i18n-attrs=\"title\" hidden>\n            <svg class=\"icon\">\n                <use xlink:href=\"#caret\"></use>\n            </svg>\n        </div>\n    </header>\n    <!-- Center -->\n    <div id=\"center\" class=\"opacity-0\">\n        <!-- Peers -->\n        <x-peers class=\"center grow-5\"></x-peers>\n        <x-no-peers class=\"center grow fade-in no-animation-on-load\" data-i18n-key=\"instructions.no-peers\" data-i18n-attrs=\"data-drop-bg\">\n            <h2 data-i18n-key=\"instructions.no-peers-title\" data-i18n-attrs=\"text\"></h2>\n            <div data-i18n-key=\"instructions.no-peers-subtitle\" data-i18n-attrs=\"text\"></div>\n        </x-no-peers>\n        <x-instructions class=\"grow fade-in\" data-i18n-key=\"instructions.x-instructions\" data-i18n-attrs=\"desktop mobile data-drop-peer data-drop-bg\"></x-instructions>\n        <div class=\"shr-panel panel column\" hidden>\n            <div class=\"row\">\n                <div class=\"thumb center\">\n                    <div class=\"text-thumb row\" hidden>\n                        <svg>\n                            <use xlink:href=\"#font\"></use>\n                        </svg>\n                        <svg>\n                            <use xlink:href=\"#i-cursor\"></use>\n                        </svg>\n                    </div>\n                    <div class=\"file-thumb\" hidden>\n                        <svg>\n                            <use xlink:href=\"#file\"></use>\n                        </svg>\n                    </div>\n                    <div class=\"image-thumb\" hidden></div>\n                </div>\n                <div class=\"share-descriptor column p-1\">\n                    <span class=\"descriptor-item\"></span>\n                    <span class=\"descriptor-other\" hidden></span>\n                </div>\n            </div>\n            <div class=\"center btn-row wrap\">\n                <div class=\"cancel-btn btn btn-small btn-rounded btn-dark text-white\" data-i18n-key=\"header.cancel-share-mode\" data-i18n-attrs=\"text\"></div>\n                <div class=\"edit-btn btn btn-small btn-rounded btn-dark text-white\" data-i18n-key=\"header.edit-share-mode\" data-i18n-attrs=\"text\" hidden></div>\n            </div>\n        </div>\n        <div id=\"websocket-fallback\" class=\"text-center\" hidden>\n            <span data-i18n-key=\"footer.traffic\" data-i18n-attrs=\"text\"></span>\n            <span data-i18n-key=\"footer.routed\" data-i18n-attrs=\"text\"></span>\n            <span data-i18n-key=\"footer.webrtc\" data-i18n-attrs=\"text\"></span>\n        </div>\n    </div>\n    <!-- Footer -->\n    <footer class=\"column opacity-0\">\n        <svg class=\"icon logo\">\n            <defs>\n                <linearGradient id=\"primaryGradient\" gradientTransform=\"rotate(90)\">\n                    <stop offset=\"0%\" class=\"start-color\" />\n                    <stop offset=\"100%\" class=\"stop-color\" />\n                </linearGradient>\n            </defs>\n            <use xlink:href=\"#wifi-tethering\" style=\"fill: url(#primaryGradient);\"></use>\n        </svg>\n        <div class=\"column\">\n            <div class=\"known-as-wrapper\">\n                <span data-i18n-key=\"footer.known-as\" data-i18n-attrs=\"text\"></span>\n                <div id=\"display-name\" class=\"badge\" data-i18n-key=\"footer.display-name\" data-i18n-attrs=\"data-placeholder title\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable></div>\n                <svg class=\"icon edit-pen\">\n                    <use xlink:href=\"#edit-pen-icon\"></use>\n                </svg>\n            </div>\n            <div class=\"discovery-wrapper panel border row\">\n                <div class=\"row center\">\n                    <span data-i18n-key=\"footer.discovery\" data-i18n-attrs=\"text\"></span>\n                </div>\n                <div class=\"row center wrap\">\n                    <span class=\"badge badge-room-ip\" data-i18n-key=\"footer.on-this-network\" data-i18n-attrs=\"text title\"></span>\n                    <span class=\"badge badge-room-secret pointer\" data-i18n-key=\"footer.paired-devices\" data-i18n-attrs=\"text title\" hidden></span>\n                    <span class=\"badge badge-room-public-id pointer\" data-i18n-key=\"footer.public-room-devices\" data-i18n-attrs=\"title\" hidden>in room IAIAI</span>\n                </div>\n            </div>\n        </div>\n    </footer>\n    <!-- Language Select Dialog -->\n    <x-dialog id=\"language-select-dialog\">\n        <x-background class=\"full center\">\n            <x-paper shadow=\"2\">\n                <div class=\"row center p-2\">\n                    <h2 class=\"dialog-title\" data-i18n-key=\"dialogs.language-selector-title\" data-i18n-attrs=\"text\"></h2>\n                </div>\n                <div class=\"language-buttons p2\">\n                    <button class=\"btn fw wrap\">\n                        <span data-i18n-key=\"dialogs.system-language\" data-i18n-attrs=\"text\"></span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"ar\">\n                        <span lang=\"ar\" dir=\"rtl\">العربية</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Arabic)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"be\">\n                        <span lang=\"be\">беларуская</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Belarusian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"nb\">\n                        <span lang=\"nb\">Bokmål</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Norwegian Bokmål)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"bg\">\n                        <span lang=\"bg\">български</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Bulgarian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"ca\">\n                        <span lang=\"ca\">Català</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Catalan)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"cs\">\n                        <span lang=\"cs\">Čeština</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Czech)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"da\">\n                        <span lang=\"da\">Dansk</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Danish)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"de\">\n                        <span lang=\"de\">Deutsch</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(German)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"en\">\n                        <span lang=\"en\">English</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"es\">\n                        <span lang=\"es\">Español</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Spanish)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"et\">\n                        <span lang=\"et\">Eesti</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Estonian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"eu\">\n                        <span lang=\"eu\">Euskara</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Basque)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"fa\">\n                        <span lang=\"fa\" dir=\"rtl\">فارسی</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Persian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"fr\">\n                        <span lang=\"fr\">Français</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(French)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"id\">\n                        <span lang=\"id\">Bahasa Indonesia</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Indonesian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"it\">\n                        <span lang=\"it\">Italiano</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Italian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"he\">\n                        <span lang=\"he\" dir=\"rtl\">עִבְרִית</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Hebrew)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"kn\">\n                        <span lang=\"kn\">ಕನ್ನಡ</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Kannada)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"hu\">\n                        <span lang=\"hu\">Magyar</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Hungarian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"nl\">\n                        <span lang=\"nl\">Nederlands</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Dutch)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"nn\">\n                        <span lang=\"nn\">Norsk</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Norwegian Nynorsk)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"pl\">\n                        <span lang=\"pl\">Polski</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Polish)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"pt-BR\">\n                        <span lang=\"pt-BR\">Português do Brasil</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Brazilian Portuguese)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"ro\">\n                        <span lang=\"ro\">Română</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Romanian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"ru\">\n                        <span lang=\"ru\">Русский язык</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Russian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"sk\">\n                        <span lang=\"sk\">Slovenčina</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Slovak)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"ta\">\n                        <span lang=\"ta\">தமிழ்</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Tamil)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"tr\">\n                        <span lang=\"tr\">Türkçe</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Turkish)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"uk\">\n                        <span lang=\"uk\">Українська</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Ukrainian)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"zh-CN\">\n                        <span lang=\"zh-CN\">汉语</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Simplified Chinese)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"zh-HK\">\n                        <span lang=\"zh-HK\">中文</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Hant Script)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"zh-TW\">\n                        <span lang=\"zh-TW\">漢語</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Traditional Chinese)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"ja\">\n                        <span lang=\"ja\">日本語</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Japanese)</span>\n                    </button>\n                    <button class=\"btn fw wrap\" value=\"ko\">\n                        <span lang=\"ko\">한국어</span>\n                        <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>\n                        <span>(Korean)</span>\n                    </button>\n                </div>\n                <div class=\"center row-reverse btn-row wrap\">\n                    <button class=\"btn btn-rounded btn-grey\" type=\"button\" data-i18n-key=\"dialogs.close\" data-i18n-attrs=\"text\" close></button>\n                </div>\n            </x-paper>\n        </x-background>\n    </x-dialog>\n    <!-- Pair Device Dialog -->\n    <x-dialog id=\"pair-device-dialog\">\n        <form action=\"#\">\n            <x-background class=\"full center text-center\">\n                <x-paper shadow=\"2\">\n                    <div class=\"row center p-2\">\n                        <h2 class=\"dialog-title\" data-i18n-key=\"dialogs.pair-devices-title\" data-i18n-attrs=\"text\"></h2>\n                    </div>\n                    <div class=\"row center p-2\">\n                        <div class=\"column\">\n                            <div class=\"center key-qr-code pointer\" data-i18n-key=\"dialogs.pair-devices-qr-code\" data-i18n-attrs=\"title\"></div>\n                            <h1 class=\"center key\" dir=\"ltr\">000 000</h1>\n                            <p class=\"center text-center key-instructions\">\n                                <span class=\"font-subheading\" data-i18n-key=\"dialogs.input-key-on-this-device\" data-i18n-attrs=\"text\"></span>\n                                <span class=\"font-subheading\" data-i18n-key=\"dialogs.scan-qr-code\" data-i18n-attrs=\"text\"></span>\n                            </p>\n                        </div>\n                    </div>\n                    <div class=\"hr-note\">\n                        <hr>\n                        <div>\n                            <span data-i18n-key=\"dialogs.hr-or\" data-i18n-attrs=\"text\"></span>\n                        </div>\n                    </div>\n                    <div class=\"row center p-2\">\n                        <div class=\"column fw\">\n                            <div class=\"input-key-container six-chars\" dir=\"ltr\">\n                                <input type=\"tel\" class=\"textarea center\" aria-label=\"pair-key-char-1\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" autofocus contenteditable placeholder disabled>\n                                <input type=\"tel\" class=\"textarea center\" aria-label=\"pair-key-char-2\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable placeholder disabled>\n                                <input type=\"tel\" class=\"textarea center\" aria-label=\"pair-key-char-3\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable placeholder disabled>\n                                <input type=\"tel\" class=\"textarea center\" aria-label=\"pair-key-char-4\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable placeholder disabled>\n                                <input type=\"tel\" class=\"textarea center\" aria-label=\"pair-key-char-5\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable placeholder disabled>\n                                <input type=\"tel\" class=\"textarea center\" aria-label=\"pair-key-char-6\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable placeholder disabled>\n                            </div>\n                            <p class=\"font-subheading center text-center\" data-i18n-key=\"dialogs.enter-key-from-another-device\" data-i18n-attrs=\"text\"></p>\n                        </div>\n                    </div>\n                    <div class=\"btn-row row-reverse wrap\">\n                        <button class=\"btn btn-rounded btn-grey\" type=\"submit\" data-i18n-key=\"dialogs.pair\" data-i18n-attrs=\"text\" disabled></button>\n                        <button class=\"btn btn-rounded btn-grey\" type=\"button\" data-i18n-key=\"dialogs.cancel\" data-i18n-attrs=\"text\" close></button>\n                    </div>\n                </x-paper>\n            </x-background>\n        </form>\n    </x-dialog>\n    <!-- Edit Paired Devices Dialog -->\n    <x-dialog id=\"edit-paired-devices-dialog\">\n        <form action=\"#\">\n            <x-background class=\"full center text-center\">\n                <x-paper shadow=\"2\">\n                    <div class=\"row center p-2\">\n                        <h2 class=\"dialog-title\" data-i18n-key=\"dialogs.edit-paired-devices-title\" data-i18n-attrs=\"text\"></h2>\n                    </div>\n                    <div class=\"paired-devices-wrapper\" data-i18n-key=\"dialogs.paired-devices-wrapper\" data-i18n-attrs=\"data-empty\"></div>\n                    <div class=\"row center p-2\">\n                        <div class=\"font-subheading\">\n                            <span data-i18n-key=\"dialogs.auto-accept-instructions-1\" data-i18n-attrs=\"text\"></span>\n                            <u data-i18n-key=\"dialogs.auto-accept\" data-i18n-attrs=\"text\"></u>\n                            <span data-i18n-key=\"dialogs.auto-accept-instructions-2\" data-i18n-attrs=\"text\"></span>\n                        </div>\n                    </div>\n                    <div class=\"center row-reverse btn-row wrap\">\n                        <button class=\"btn btn-rounded btn-grey\" type=\"button\" data-i18n-key=\"dialogs.close\" data-i18n-attrs=\"text\" close></button>\n                    </div>\n                </x-paper>\n            </x-background>\n        </form>\n    </x-dialog>\n    <!-- Public Room Dialog -->\n    <x-dialog id=\"public-room-dialog\">\n        <form action=\"#\">\n            <x-background class=\"full center text-center\">\n                <x-paper shadow=\"2\">\n                    <div class=\"row center p-2\">\n                        <h2 class=\"dialog-title\" data-i18n-key=\"dialogs.temporary-public-room-title\" data-i18n-attrs=\"text\"></h2>\n                    </div>\n                    <div class=\"row center p-2\">\n                        <div class=\"column\">\n                            <div class=\"center key-qr-code pointer\" data-i18n-key=\"dialogs.public-room-qr-code\" data-i18n-attrs=\"title\"></div>\n                            <h1 class=\"center key\" dir=\"ltr\"></h1>\n                            <p class=\"center text-center key-instructions\">\n                                <span class=\"font-subheading\" data-i18n-key=\"dialogs.input-room-id-on-another-device\" data-i18n-attrs=\"text\"></span>\n                                <span class=\"font-subheading\" data-i18n-key=\"dialogs.scan-qr-code\" data-i18n-attrs=\"text\"></span>\n                            </p>\n                        </div>\n                    </div>\n                    <div class=\"hr-note\">\n                        <hr>\n                        <div>\n                            <span data-i18n-key=\"dialogs.hr-or\" data-i18n-attrs=\"text\"></span>\n                        </div>\n                    </div>\n                    <div class=\"row center p-2\">\n                        <div class=\"column fw\">\n                            <div class=\"input-key-container\" dir=\"ltr\">\n                                <input type=\"text\" class=\"textarea center\" aria-label=\"room-id-char-1\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" autofocus contenteditable placeholder disabled>\n                                <input type=\"text\" class=\"textarea center\" aria-label=\"room-id-char-2\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable placeholder disabled>\n                                <input type=\"text\" class=\"textarea center\" aria-label=\"room-id-char-3\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable placeholder disabled>\n                                <input type=\"text\" class=\"textarea center\" aria-label=\"room-id-char-4\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable placeholder disabled>\n                                <input type=\"text\" class=\"textarea center\" aria-label=\"room-id-char-5\" maxlength=\"1\" autocorrect=\"off\" autocomplete=\"off\" autocapitalize=\"none\" spellcheck=\"false\" contenteditable placeholder disabled>\n                            </div>\n                            <p class=\"font-subheading center text-center\" data-i18n-key=\"dialogs.enter-room-id-from-another-device\" data-i18n-attrs=\"text\"></p>\n                        </div>\n                    </div>\n                    <div class=\"center row-reverse btn-row wrap\">\n                        <div class=\"row-reverse wrap grow-2\">\n                            <button class=\"btn btn-rounded btn-grey\" type=\"submit\" data-i18n-key=\"dialogs.join\" data-i18n-attrs=\"text\" disabled></button>\n                            <button class=\"btn btn-rounded btn-grey leave-room\" type=\"button\" data-i18n-key=\"dialogs.leave\" data-i18n-attrs=\"text\"></button>\n                        </div>\n                        <button class=\"btn btn-rounded btn-grey\" type=\"button\" data-i18n-key=\"dialogs.close\" data-i18n-attrs=\"text\" close></button>\n                    </div>\n                </x-paper>\n            </x-background>\n        </form>\n    </x-dialog>\n    <!-- Receive Request Dialog -->\n    <x-dialog id=\"receive-request-dialog\">\n        <x-background class=\"full center\">\n            <x-paper shadow=\"2\">\n                <div class=\"row center p-2\">\n                    <h2 class=\"dialog-title\"></h2>\n                </div>\n                <div class=\"row center p-2\">\n                    <div class=\"column center file-description\">\n                        <div>\n                            <span class=\"display-name badge\"></span>\n                            <span data-i18n-key=\"dialogs.would-like-to-share\" data-i18n-attrs=\"text\"></span>\n                        </div>\n                        <div class=\"row file-name\">\n                            <span class=\"file-stem\"></span>\n                            <span class=\"file-extension\"></span>\n                        </div>\n                        <div class=\"row file-other\">\n                        </div>\n                        <div class=\"row font-body2 file-size\"></div>\n                    </div>\n                </div>\n                <div class=\"center file-preview\"></div>\n                <div class=\"row-reverse center btn-row wrap\">\n                    <button id=\"accept-request\" class=\"btn btn-rounded btn-grey\" title=\"ENTER\" data-i18n-key=\"dialogs.accept\" data-i18n-attrs=\"text\" autofocus disabled></button>\n                    <button id=\"decline-request\" class=\"btn btn-rounded btn-grey\" title=\"ESCAPE\" data-i18n-key=\"dialogs.decline\" data-i18n-attrs=\"text\"></button>\n                </div>\n            </x-paper>\n        </x-background>\n    </x-dialog>\n    <!-- Receive File Dialog -->\n    <x-dialog id=\"receive-file-dialog\">\n        <x-background class=\"full center\">\n            <x-paper shadow=\"2\">\n                <div class=\"row center p-2\">\n                    <h2 class=\"dialog-title\"></h2>\n                </div>\n                <div class=\"row center p-2\">\n                    <div class=\"column center file-description\">\n                        <div>\n                            <span class=\"display-name badge\"></span>\n                            <span data-i18n-key=\"dialogs.has-sent\" data-i18n-attrs=\"text\"></span>\n                        </div>\n                        <div class=\"row file-name\">\n                            <span class=\"file-stem\"></span>\n                            <span class=\"file-extension\"></span>\n                        </div>\n                        <div class=\"row file-other\">\n                        </div>\n                        <div class=\"row font-body2 file-size\"></div>\n                    </div>\n                </div>\n                <div class=\"center file-preview\"></div>\n                <div class=\"row-reverse center btn-row wrap\">\n                    <button id=\"share-btn\" class=\"btn btn-rounded btn-grey\" data-i18n-key=\"dialogs.share\" data-i18n-attrs=\"text\" hidden></button>\n                    <button id=\"download-btn\" class=\"btn btn-rounded btn-grey\" data-i18n-key=\"dialogs.download\" data-i18n-attrs=\"text\" autofocus disabled></button>\n                    <button class=\"btn btn-rounded btn-grey\" data-i18n-key=\"dialogs.close\" data-i18n-attrs=\"text\" close></button>\n                </div>\n            </x-paper>\n        </x-background>\n    </x-dialog>\n    <!-- Send Text Dialog -->\n    <x-dialog id=\"send-text-dialog\">\n        <form action=\"#\">\n            <x-background class=\"full center\">\n                <x-paper shadow=\"2\">\n                    <div class=\"row center p-2\">\n                        <h2 class=\"dialog-title\" data-i18n-key=\"dialogs.send-message-title\" data-i18n-attrs=\"text\"></h2>\n                    </div>\n                    <div class=\"row center p-2 display-name-wrapper\">\n                        <div class=\"column\">\n                            <div class=\"text-center\">\n                                <span data-i18n-key=\"dialogs.send-message-to\" data-i18n-attrs=\"text\"></span>\n                                <span class=\"display-name badge\"></span>\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"row p-2\">\n                        <div class=\"column fw\">\n                            <div class=\"fw textarea\" role=\"textbox\" data-i18n-key=\"dialogs.message\" data-i18n-attrs=\"title placeholder\" autofocus contenteditable></div>\n                        </div>\n                    </div>\n                    <div class=\"btn-row row-reverse wrap\">\n                        <button class=\"btn btn-rounded btn-grey\" type=\"submit\" title=\"CTRL/⌘ + ENTER\" data-i18n-key=\"dialogs.send\" data-i18n-attrs=\"text\" disabled></button>\n                        <button class=\"btn btn-rounded btn-grey\" type=\"button\" title=\"ESCAPE\" data-i18n-key=\"dialogs.cancel\" data-i18n-attrs=\"text\" close></button>\n                    </div>\n                </x-paper>\n            </x-background>\n        </form>\n    </x-dialog>\n    <!-- Receive Text Dialog -->\n    <x-dialog id=\"receive-text-dialog\">\n        <x-background class=\"full center\">\n            <x-paper shadow=\"2\">\n                <div class=\"row center p-2\">\n                    <h2 class=\"dialog-title\" class=\"text-center\" data-i18n-key=\"dialogs.receive-text-title\" data-i18n-attrs=\"text\"></h2>\n                </div>\n                <div class=\"row center p-2 display-name-wrapper\">\n                    <div class=\"text-center\">\n                        <span class=\"display-name badge\"></span>\n                        <span data-i18n-key=\"dialogs.has-sent\" data-i18n-attrs=\"text\"></span>\n                    </div>\n                </div>\n                <div class=\"row center p-2\">\n                    <div class=\"column fw\">\n                        <div id=\"text\" class=\"textarea\"></div>\n                    </div>\n                </div>\n                <div class=\"row-reverse center btn-row wrap\">\n                    <button id=\"copy\" class=\"btn btn-rounded btn-grey\" title=\"CTRL/⌘ + C\" data-i18n-key=\"dialogs.copy\" data-i18n-attrs=\"text\"></button>\n                    <button id=\"close\" class=\"btn btn-rounded btn-grey\" title=\"ESCAPE\" data-i18n-key=\"dialogs.close\" data-i18n-attrs=\"text\"></button>\n                </div>\n            </x-paper>\n        </x-background>\n    </x-dialog>\n    <!-- Share Text Dialog -->\n    <x-dialog id=\"share-text-dialog\">\n        <x-background class=\"full center\">\n            <x-paper shadow=\"2\">\n                <div class=\"row center p-2\">\n                    <h2 class=\"dialog-title\" data-i18n-key=\"dialogs.share-text-title\" data-i18n-attrs=\"text\"></h2>\n                </div>\n                <div class=\"row center p-2 pb-0\">\n                    <div class=\"column\">\n                        <div class=\"text-center\">\n                            <span data-i18n-key=\"dialogs.share-text-subtitle\" data-i18n-attrs=\"text\"></span>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"row p-2\">\n                    <div class=\"column fw\">\n                        <div class=\"fw textarea\" role=\"textbox\" data-i18n-key=\"dialogs.message\" data-i18n-attrs=\"title placeholder\" contenteditable></div>\n                    </div>\n                </div>\n                <div class=\"row p-2 center\">\n                    <span class=\"mx-1\" data-i18n-key=\"dialogs.share-text-checkbox\" data-i18n-attrs=\"text\"></span>\n                    <label class=\"pointer switch mx-1\">\n                        <input type=\"checkbox\">\n                        <div class=\"slider round\"></div>\n                    </label>\n                </div>\n                <div class=\"btn-row row-reverse wrap\">\n                    <button class=\"btn btn-rounded btn-grey\" type=\"submit\" title=\"CTRL/⌘ + ENTER\" data-i18n-key=\"dialogs.approve\" data-i18n-attrs=\"text\" autofocus disabled></button>\n                </div>\n            </x-paper>\n        </x-background>\n    </x-dialog>\n    <!-- base64 Paste Dialog -->\n    <x-dialog id=\"base64-paste-dialog\">\n        <x-background class=\"full center\">\n            <x-paper shadow=\"2\">\n                <div class=\"row center p-2\">\n                    <h2 class=\"dialog-title\"></h2>\n                </div>\n                <div class=\"row p-2\">\n                    <button class=\"btn btn-rounded btn-grey center\" id=\"base64-paste-btn\" title=\"Paste\"></button>\n                    <div class=\"textarea\" title=\"CMD/⌘ + V\" contenteditable hidden></div>\n                </div>\n                <div class=\"row-reverse center btn-row wrap\">\n                    <button class=\"btn btn-rounded btn-grey\" data-i18n-key=\"dialogs.close\" data-i18n-attrs=\"text\" close></button>\n                </div>\n            </x-paper>\n        </x-background>\n    </x-dialog>\n    <!-- Toast -->\n    <div class=\"toast-container full center\">\n        <x-toast id=\"toast\" shadow=\"1\">\n            <span class=\"center text-center\"></span>\n            <div class=\"icon-button\" data-i18n-key=\"dialogs.close-toast\" data-i18n-attrs=\"title\">\n                <svg class=\"icon\">\n                    <use xlink:href=\"#close-icon\"></use>\n                </svg>\n            </div>\n        </x-toast>\n    </div>\n    <!-- About Page -->\n    <x-about id=\"about\" class=\"full center column\">\n        <header class=\"row-reverse\">\n            <a href=\"#\" class=\"close icon-button\" data-i18n-key=\"about.close-about\" data-i18n-attrs=\"aria-label\">\n                <svg class=\"icon\">\n                    <use xlink:href=\"#close-icon\"></use>\n                </svg>\n            </a>\n        </header>\n        <section class=\"center column\">\n            <svg class=\"icon logo\">\n                <use xlink:href=\"#wifi-tethering\"></use>\n            </svg>\n            <div class=\"title-wrapper\" dir=\"ltr\">\n                <h1>PairDrop</h1>\n                <div class=\"font-subheading\">v1.11.2</div>\n            </div>\n            <div class=\"font-subheading\" data-i18n-key=\"about.claim\" data-i18n-attrs=\"text\"></div>\n            <div class=\"row\">\n                <a class=\"icon-button\" target=\"_blank\" href=\"https://github.com/schlagmichdoch/pairdrop\" rel=\"noreferrer\" data-i18n-key=\"about.github\" data-i18n-attrs=\"title\">\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#github\"></use>\n                    </svg>\n                </a>\n                <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\">\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#donation\"></use>\n                    </svg>\n                </a>\n                <a class=\"icon-button\" id=\"x-twitter-btn\" target=\"_blank\" href=\"https://x.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\">\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#x-twitter\"></use>\n                    </svg>\n                </a>\n                <a class=\"icon-button\" id=\"mastodon-btn\" target=\"_blank\" rel=\"noreferrer\" data-i18n-key=\"about.mastodon\" data-i18n-attrs=\"title\" hidden>\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#mastodon\"></use>\n                    </svg>\n                </a>\n                <a class=\"icon-button\" id=\"bluesky-btn\" target=\"_blank\" rel=\"noreferrer\" data-i18n-key=\"about.bluesky\" data-i18n-attrs=\"title\" hidden>\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#bluesky\"></use>\n                    </svg>\n                </a>\n                <a class=\"icon-button\" id=\"custom-btn\" target=\"_blank\" rel=\"noreferrer\" data-i18n-key=\"about.custom\" data-i18n-attrs=\"title\" hidden>\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#custom\"></use>\n                    </svg>\n                </a>\n                <a class=\"icon-button\" id=\"privacypolicy-btn\" target=\"_blank\" rel=\"noreferrer\" data-i18n-key=\"about.privacypolicy\" data-i18n-attrs=\"title\" hidden>\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#privacypolicy\"></use>\n                    </svg>\n                </a>\n                <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\">\n                    <svg class=\"icon\">\n                        <use xlink:href=\"#help-outline\"></use>\n                    </svg>\n                </a>\n            </div>\n        </section>\n        <x-background></x-background>\n    </x-about>\n    <canvas class=\"circles opacity-0\"></canvas>\n    <!-- SVG Icon Library -->\n    <svg style=\"display: none;\">\n        <symbol id=\"wifi-tethering\" viewBox=\"0 0 24 24\">\n            <path d=\"M12 11c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 2c0-3.31-2.69-6-6-6s-6 2.69-6 6c0 2.22 1.21 4.15 3 5.19l1-1.74c-1.19-.7-2-1.97-2-3.45 0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.48-.81 2.75-2 3.45l1 1.74c1.79-1.04 3-2.97 3-5.19zM12 3C6.48 3 2 7.48 2 13c0 3.7 2.01 6.92 4.99 8.65l1-1.73C5.61 18.53 4 15.96 4 13c0-4.42 3.58-8 8-8s8 3.58 8 8c0 2.96-1.61 5.53-4 6.92l1 1.73c2.99-1.73 5-4.95 5-8.65 0-5.52-4.48-10-10-10z\"></path>\n        </symbol>\n        <symbol id=\"desktop-mac\" viewBox=\"0 0 24 24\">\n            <path d=\"M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7l-2 3v1h8v-1l-2-3h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 12H3V4h18v10z\"></path>\n        </symbol>\n        <symbol id=\"phone-iphone\" viewBox=\"0 0 24 24\">\n            <path d=\"M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z\"></path>\n        </symbol>\n        <symbol id=\"tablet-mac\" viewBox=\"0 0 24 24\">\n            <path d=\"M18.5 0h-14C3.12 0 2 1.12 2 2.5v19C2 22.88 3.12 24 4.5 24h14c1.38 0 2.5-1.12 2.5-2.5v-19C21 1.12 19.88 0 18.5 0zm-7 23c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-4H4V3h15v16z\"></path>\n        </symbol>\n        <symbol id=\"info-outline\" viewBox=\"0 0 24 24\">\n            <path d=\"M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z\"></path>\n        </symbol>\n        <symbol id=\"close-icon\" viewBox=\"0 0 24 24\">\n            <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"></path>\n        </symbol>\n        <symbol id=\"help-outline\" viewBox=\"0 0 24 24\">\n            <path d=\"M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z\"></path>\n        </symbol>\n        <symbol id=\"x-twitter\">\n            <path d=\"M17.996,2.219l3.265,0l-7.13,8.148l8.388,11.088l-6.566,0l-5.147,-6.723l-5.882,6.723l-3.269,0l7.625,-8.716l-8.041,-10.52l6.733,0l4.647,6.146l5.377,-6.146Zm-1.146,17.285l1.808,-0l-11.671,-15.435l-1.942,0l11.805,15.435Z\"></path>\n        </symbol>\n        <symbol id=\"github\">\n            <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"></path>\n        </symbol>\n        <symbol id=\"notifications\">\n            <path d=\"M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z\"></path>\n        </symbol>\n        <symbol id=\"homescreen\">\n            <path fill=\"none\" d=\"M0 0h24v24H0V0z\"></path>\n            <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>\n            <path fill=\"none\" d=\"M0 0h24v24H0V0z\"></path>\n        </symbol>\n        <symbol id=\"donation\">\n            <path d=\"M0 0h24v24H0z\" fill=\"none\"></path>\n            <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>\n        </symbol>\n        <symbol id=\"icon-theme-auto\" viewBox=\"-54 -54 620 620\">\n            <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->\n            <path d=\"M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z\"></path>\n        </symbol>\n        <symbol id=\"icon-theme-light\" viewBox=\"-54 -54 620 620\">\n            <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->\n            <path d=\"M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z\"></path>\n        </symbol>\n        <symbol id=\"icon-theme-dark\" viewBox=\"0 0 24 24\">\n            <rect fill=\"none\" height=\"24\" width=\"24\"></rect><path d=\"M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z\"></path>\n        </symbol>\n        <symbol id=\"pair-device-icon\" viewBox=\"0 0 640 512\">\n            <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->\n            <path d=\"M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z\"></path>\n        </symbol>\n        <symbol id=\"edit-pair-devices-icon\" viewBox=\"-159 25 640 512\">\n            <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->\n            <!--! edited by @schlagmichdoch -->\n            <path d=\"M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.4-10.3,17.7-30.3,7.4-44.6s-30.3-17.7-44.6-7.4l-1.6,1.1c-32.1,22.9-76,19.3-103.8-8.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-14.4,10.3-17.7,30.3-7.4,44.6s30.3,17.7,44.6,7.4l1.6-1.1c32.1-22.9,76-19.3,103.8,8.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z\"></path>\n            <path d=\"M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z\"></path>\n        </symbol>\n        <symbol id=\"edit-pen-icon\" viewBox=\"0 0 512 512\">\n            <!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->\n            <path d=\"M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z\"></path>\n        </symbol>\n        <symbol id=\"public-room-icon\" viewBox=\"0 0 640 512\">\n            <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->\n            <path d=\"M0 24C0 10.7 10.7 0 24 0H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24C10.7 48 0 37.3 0 24zM0 488c0-13.3 10.7-24 24-24H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24zM83.2 160a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM32 320c0-35.3 28.7-64 64-64h96c12.2 0 23.7 3.4 33.4 9.4c-37.2 15.1-65.6 47.2-75.8 86.6H64c-17.7 0-32-14.3-32-32zm461.6 32c-10.3-40.1-39.6-72.6-77.7-87.4c9.4-5.5 20.4-8.6 32.1-8.6h96c35.3 0 64 28.7 64 64c0 17.7-14.3 32-32 32H493.6zM391.2 290.4c32.1 7.4 58.1 30.9 68.9 61.6c3.5 10 5.5 20.8 5.5 32c0 17.7-14.3 32-32 32h-224c-17.7 0-32-14.3-32-32c0-11.2 1.9-22 5.5-32c10.5-29.7 35.3-52.8 66.1-60.9c7.8-2.1 16-3.1 24.5-3.1h96c7.4 0 14.7 .8 21.6 2.4zm44-130.4a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM321.6 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z\"></path>\n        </symbol>\n        <symbol id=\"icon-language-selector\" viewBox=\"0 0 640 512\">\n            <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->\n            <path d=\"M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z\"></path>\n        </symbol>\n        <symbol id=\"i-cursor\" viewBox=\"-180 0 640 512\">\n            <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->\n            <path d=\"M.1 29.3C-1.4 47 11.7 62.4 29.3 63.9l8 .7C70.5 67.3 96 95 96 128.3V224H64c-17.7 0-32 14.3-32 32s14.3 32 32 32H96v95.7c0 33.3-25.5 61-58.7 63.8l-8 .7C11.7 449.6-1.4 465 .1 482.7s16.9 30.7 34.5 29.2l8-.7c34.1-2.8 64.2-18.9 85.4-42.9c21.2 24 51.2 40.1 85.4 42.9l8 .7c17.6 1.5 33.1-11.6 34.5-29.2s-11.6-33.1-29.2-34.5l-8-.7C185.5 444.7 160 417 160 383.7V288h32c17.7 0 32-14.3 32-32s-14.3-32-32-32H160V128.3c0-33.3 25.5-61 58.7-63.8l8-.7c17.6-1.5 30.7-16.9 29.2-34.5S239-1.4 221.3 .1l-8 .7C179.2 3.6 149.2 19.7 128 43.7c-21.2-24-51.2-40-85.4-42.9l-8-.7C17-1.4 1.6 11.7 .1 29.3z\"></path>\n        </symbol>\n        <symbol id=\"font\" viewBox=\"-100 0 640 512\">\n            <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->\n            <path d=\"M254 52.8C249.3 40.3 237.3 32 224 32s-25.3 8.3-30 20.8L57.8 416H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32h-1.8l18-48H303.8l18 48H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H390.2L254 52.8zM279.8 304H168.2L224 155.1 279.8 304z\"></path>\n        </symbol>\n        <symbol id=\"file\" viewBox=\"-130 0 650 530\">\n            <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->\n            <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>\n        </symbol>\n        <symbol id=\"caret\" viewBox=\"0 0 320 512\">\n            <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->\n            <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>\n        </symbol>\n        <symbol id=\"mastodon\" viewBox=\"0 0 448 512\">\n            <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->\n            <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>\n        </symbol>\n        <symbol id=\"bluesky\" viewBox=\"0 0 512 512\">\n            <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->\n            <path d=\"M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z\"/>\n        </symbol>\n        <symbol id=\"custom\" viewBox=\"0 0 512 512\">\n            <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->\n            <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>\n        </symbol>\n        <symbol id=\"privacypolicy\" viewBox=\"0 0 512 512\">\n            <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->\n            <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>\n        </symbol>\n    </svg>\n    <!-- Scripts -->\n    <script src=\"scripts/localization.js\" defer></script>\n    <script src=\"scripts/persistent-storage.js\" defer></script>\n    <script src=\"scripts/ui-main.js\" defer></script>\n    <script src=\"scripts/main.js\" defer></script>\n    <!-- Sounds -->\n    <audio id=\"blop\" autobuffer=\"true\">\n        <source src=\"sounds/blop.mp3\" type=\"audio/mpeg\">\n        <source src=\"sounds/blop.ogg\" type=\"audio/ogg\">\n    </audio>\n    <!-- no script -->\n    <noscript>\n        <x-noscript class=\"full center column\">\n            <h1>Enable JavaScript</h1>\n            <h3>PairDrop works only with JavaScript</h3>\n        </x-noscript>\n    </noscript>\n</body>\n</html>\n"
  },
  {
    "path": "public/lang/ar.json",
    "content": "{\n    \"footer\": {\n        \"webrtc\": \"إذا لم يكن WebRTC متاحًا.\",\n        \"public-room-devices_title\": \"يمكن اكتشافك بواسطة الأجهزة الموجودة في هذه الغرفة العامة المستقلة عن الشبكة.\",\n        \"display-name_data-placeholder\": \"تحميل …\",\n        \"display-name_title\": \"قم بتحرير اسم جهازك بشكل دائم\",\n        \"traffic\": \"حركة المرور هي\",\n        \"paired-devices_title\": \"يمكن اكتشافك بواسطة الأجهزة المقترنة في جميع الأوقات بشكل مستقل عن الشبكة.\",\n        \"public-room-devices\": \"في الغرفة {{roomId}}\",\n        \"paired-devices\": \"بواسطة الأجهزة المقترنة\",\n        \"on-this-network\": \"على هذه الشبكة\",\n        \"routed\": \"توجيهّا من خلال الخادم\",\n        \"discovery\": \"يمكنك اكتشافك:\",\n        \"on-this-network_title\": \"يمكن للجميع اكتشافك على هذه الشبكة.\",\n        \"known-as\": \"‌أنت معروف بأنك:\"\n    },\n    \"notifications\": {\n        \"request-title\": \"يرغب {{name}} في نقل {{count}} {{descriptor}}\",\n        \"unfinished-transfers-warning\": \"هناك تحويلات غير مكتملة. هل أنت متأكد أنك تريد إغلاق PairDrop؟\",\n        \"message-received\": \"تم استلام الرسالة بواسطة {{name}} - انقر للفتح\",\n        \"rate-limit-join-key\": \"تم الوصول إلى الحد الأقصى. انتظر 10 ثوان وحاول مرة أخرى.\",\n        \"connecting\": \"يتصل …\",\n        \"pairing-key-invalidated\": \"المفتاح {{key}} خاطئ\",\n        \"pairing-key-invalid\": \"مُفتاح خاطئ\",\n        \"connected\": \"متصل\",\n        \"pairing-not-persistent\": \"الأجهزة المقترنة ليست ثابتة\",\n        \"text-content-incorrect\": \"محتوى النص غير صحيح\",\n        \"message-transfer-completed\": \"اكتمل نقل الرسالة\",\n        \"file-transfer-completed\": \"اكتمل نقل الملف\",\n        \"file-content-incorrect\": \"محتوى الملف غير صحيح\",\n        \"files-incorrect\": \"الملفات غير صحيحة\",\n        \"selected-peer-left\": \"مُحَدد الاجهزة المقترنة\",\n        \"link-received\": \"تم استلام الرابط بواسطة {{name}} - انقر للفتح\",\n        \"online\": \"لقد عدت متصلاً بالإنترنت\",\n        \"public-room-left\": \"الخروج من الغرفة العامة {{publicRoomId}}\",\n        \"copied-text\": \"نُسِخَ النص إلى الحافظة\",\n        \"display-name-random-again\": \"يتم إنشاء اسم العرض بشكل عشوائي مرة أخرى\",\n        \"display-name-changed-permanently\": \"يتم تغيير اسم العرض بشكل دائم\",\n        \"copied-to-clipboard-error\": \"النسخ غير ممكن. انسخ يدويًا.\",\n        \"pairing-success\": \"الأجهزة المقترنة\",\n        \"clipboard-content-incorrect\": \"محتوى الحافظة غير صحيح\",\n        \"display-name-changed-temporarily\": \"تم تغيير اسم العرض لهذه الجلسة فقط\",\n        \"copied-to-clipboard\": \"تم النسخ إلى الحافظة\",\n        \"offline\": \"انت غير متصل\",\n        \"pairing-tabs-error\": \"من المستحيل إقران علامتي تبويب متصفح الويب\",\n        \"public-room-id-invalid\": \"معرف الغرفة غير صالح\",\n        \"click-to-download\": \"إضغط للتحميل\",\n        \"pairing-cleared\": \"جميع الأجهزة غير مقترنة\",\n        \"notifications-enabled\": \"تم تمكين الإشعارات\",\n        \"online-requirement-pairing\": \"يجب أن تكون متصلاً بالإنترنت لإقران الأجهزة\",\n        \"ios-memory-limit\": \"لا يمكن إرسال ملفات إلى iOS إلا بحجم يصل إلى 200 ميجابايت مرة واحدة\",\n        \"online-requirement-public-room\": \"يجب أن تكون متصلاً بالإنترنت لإنشاء غرفة عامة\",\n        \"copied-text-error\": \"فشلت الكتابة من الحافظة. انسخ يدويًا!\",\n        \"download-successful\": \"تم تحميل {{descriptor}}\",\n        \"click-to-show\": \"اضغط للعرض\",\n        \"pair-url-copied-to-clipboard\": \"تم نسخ رابط اقتران هذا الجهاز إلى الحافظة\",\n        \"room-url-copied-to-clipboard\": \"تم نسخ رابط هذه الغرفة العامة إلى الحافظة\",\n        \"notifications-permissions-error\": \"لم يتم منح إذن الإشعارات حيث أن المستخدم أغلق نافذة السماح عدة مرات. يمكن إعادة تعيين هذا في معلومات الصفحة التي يمكن فتحها بالضغط على رمز القفل بجانب شريط عنوان صفحة الإنترنت.\"\n    },\n    \"header\": {\n        \"cancel-share-mode\": \"تمّ\",\n        \"theme-auto_title\": \"تغيير المظهر تلقائيا من النظام\",\n        \"install_title\": \"تثبيت PairDrop\",\n        \"theme-dark_title\": \"إستخدم دائما المظهر المظلم\",\n        \"pair-device_title\": \"قم بإقران أجهزتك بشكل دائم\",\n        \"join-public-room_title\": \"انضم إلى الغرفة العامة مؤقتًا\",\n        \"notification_title\": \"تفعيل الإشعارات\",\n        \"edit-paired-devices_title\": \"عدل الأجهزة المقترنة\",\n        \"language-selector_title\": \"إختر اللغة\",\n        \"about_title\": \"حول PairDrop\",\n        \"about_aria-label\": \"افتح حول PairDrop\",\n        \"theme-light_title\": \"إستخدم دائماً المظهر الفاتح\",\n        \"edit-share-mode\": \"عدل\",\n        \"expand_title\": \"Expand header button row\"\n    },\n    \"instructions\": {\n        \"x-instructions_mobile\": \"انقر لإرسال الملفات أو انقر لفترة طويلة لإرسال رسالة\",\n        \"x-instructions-share-mode_desktop\": \"انقر للإرسال {{descriptor}}\",\n        \"activate-share-mode-and-other-files-plural\": \"و{{count}} ملفات أخرى\",\n        \"x-instructions-share-mode_mobile\": \"انقر للإرسال {{descriptor}}\",\n        \"activate-share-mode-base\": \"افتح PairDrop على الأجهزة الأخرى للإرسال\",\n        \"no-peers-subtitle\": \"قم بإقران الأجهزة أو ادخل إلى غرفة عامة لتتمكن من أن تكتشف على الشبكات الأخرى\",\n        \"activate-share-mode-shared-text\": \"النص المشترك\",\n        \"x-instructions_desktop\": \"انقر لإرسال الملفات أو انقر بزر الفأرة الأيمن لإرسال رسالة\",\n        \"no-peers-title\": \"افتح PairDrop على الأجهزة الأخرى لإرسال الملفات\",\n        \"x-instructions_data-drop-bg\": \"حرر لتحديد المستلم\",\n        \"no-peers_data-drop-bg\": \"حرر لتحديد المستلم\",\n        \"x-instructions_data-drop-peer\": \"قم بالتحرير لإرسالها إلى القرين\",\n        \"activate-share-mode-and-other-file\": \"وملف واحد آخر\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} ملفات مشاركة\",\n        \"activate-share-mode-shared-file\": \"الملف المُشارك\",\n        \"webrtc-requirement\": \"لتستعمل بيردروب هنا، يجب أن يكون WebRTC مفعلًا!\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"مُعالجة …\",\n        \"click-to-send-share-mode\": \"انقر للإرسال {{descriptor}}\",\n        \"click-to-send\": \"انقر لإرسال الملفات أو انقر بزر الماوس الأيمن لإرسال رسالة\",\n        \"waiting\": \"يُرجى الإنتظار…\",\n        \"connection-hash\": \"للتحقق من أمان التشفير الشامل، قم بمقارنة رقم الأمان هذا على كلا الجهازين\",\n        \"preparing\": \"يقترن…\",\n        \"transferring\": \"جارٍ النقل…\"\n    },\n    \"dialogs\": {\n        \"base64-paste-to-send\": \"الصق هنا للمشاركة {{type}}\",\n        \"auto-accept-instructions-2\": \"لقبول جميع الملفات المرسلة من هذا الجهاز تلقائيًا.\",\n        \"receive-text-title\": \"تلقيت رسالة\",\n        \"edit-paired-devices-title\": \"تحرير الأجهزة المقترنة\",\n        \"cancel\": \"إلغاء\",\n        \"auto-accept-instructions-1\": \"تفعيل\",\n        \"pair-devices-title\": \"إقران الأجهزة بشكل دائم\",\n        \"download\": \"تحميل\",\n        \"title-file\": \"ملف\",\n        \"base64-processing\": \"مُعالجة…\",\n        \"decline\": \"رفض\",\n        \"receive-title\": \"تم استلام {{descriptor}}\",\n        \"leave\": \"مُغادرة\",\n        \"join\": \"انضمام\",\n        \"title-image-plural\": \"صور\",\n        \"send\": \"ارسال\",\n        \"base64-tap-to-paste\": \"انقر هنا للمشاركة {{type}}\",\n        \"base64-text\": \"نص\",\n        \"copy\": \"نسخ\",\n        \"file-other-description-image\": \"وصورة واحدة أخرى\",\n        \"temporary-public-room-title\": \"غرفة عامة مؤقتة\",\n        \"base64-files\": \"ملفات\",\n        \"has-sent\": \"ارسلت:\",\n        \"file-other-description-file\": \"وملف واحد آخر\",\n        \"close\": \"إغلاق\",\n        \"system-language\": \"لغة النظام\",\n        \"unpair\": \"إلغاء الإقتران\",\n        \"title-image\": \"صورة\",\n        \"file-other-description-file-plural\": \"و{{count}} ملفات أخرى\",\n        \"would-like-to-share\": \"ترغب في المشاركة\",\n        \"send-message-to\": \"أرسال رسالة إلى:\",\n        \"language-selector-title\": \"إختر اللُغة\",\n        \"pair\": \"إقتران\",\n        \"hr-or\": \"او\",\n        \"scan-qr-code\": \"أو مسح رمز الاستجابة السريعة.\",\n        \"input-key-on-this-device\": \"أدخل هذا المفتاح على جهاز آخر\",\n        \"download-again\": \"تحميل مرة أخرى\",\n        \"accept\": \"قبول\",\n        \"paired-devices-wrapper_data-empty\": \"لا توجد أجهزة مقترنة.\",\n        \"enter-key-from-another-device\": \"أدخل المفتاح من جهاز آخر هنا.\",\n        \"share\": \"مُشاركة\",\n        \"auto-accept\": \"قبول تلقائي\",\n        \"title-file-plural\": \"ملفات\",\n        \"send-message-title\": \"إرسال رسالة\",\n        \"input-room-id-on-another-device\": \"‌أدخل معرف الغرفة هذا على جهاز آخر\",\n        \"file-other-description-image-plural\": \"و{{count}} صور أخرى\",\n        \"enter-room-id-from-another-device\": \"أدخل معرف الغرفة من جهاز آخر للانضمام إلى الغرفة.\",\n        \"share-text-title\": \"شارك رسالة نصية\",\n        \"paired-device-removed\": \"تمت إزالة الجهاز المقترن.\",\n        \"message_title\": \"أدخل رسالة لإرسالها\",\n        \"message_placeholder\": \"النص\",\n        \"base64-title-files\": \"شارك ملفات\",\n        \"base64-title-text\": \"شارك نصًا\",\n        \"public-room-qr-code_title\": \"اضغط لنسخ رابط الغرفة العامة\",\n        \"approve\": \"قبول\",\n        \"share-text-subtitle\": \"عدل الرسالة قبل الإرسال:\",\n        \"share-text-checkbox\": \"أظهر هذه الرسالة دائمًا عند مشاركة النصوص\",\n        \"close-toast_title\": \"أغلق الإشعار\",\n        \"pair-devices-qr-code_title\": \"اضغط لنسخ رابط اقتران هذا الجهاز\"\n    },\n    \"about\": {\n        \"claim\": \"أسهل طريقة لنقل الملفات عبر الأجهزة\",\n        \"tweet_title\": \"غرّد حول PairDrop\",\n        \"close-about_aria-label\": \"إغلاق حول PairDrop\",\n        \"buy-me-a-coffee_title\": \"اشتري لي القهوة!\",\n        \"github_title\": \"PairDrop على جيت هاب\",\n        \"faq_title\": \"أسئلة متكررة\",\n        \"mastodon_title\": \"اكتب عن بيردروب على ماستادون\",\n        \"bluesky_title\": \"تابعنا على بلوسكاي\",\n        \"custom_title\": \"تابعنا\",\n        \"privacypolicy_title\": \"افتح سياسة الخصوصية الخاصة بنا\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"طلب نقل الملف\",\n        \"message-received-plural\": \"{{count}} الرسائل المستلمة\",\n        \"message-received\": \"تم إرسال الرسالة\",\n        \"file-received\": \"تم استلام الملف\",\n        \"file-received-plural\": \"{{count}} الملفات المستلمة\",\n        \"image-transfer-requested\": \"طُلب نقل الصور المطلوبة\"\n    }\n}\n"
  },
  {
    "path": "public/lang/be.json",
    "content": "{\n    \"header\": {\n        \"about_aria-label\": \"Адкрыйце Аб PairDrop\",\n        \"about_title\": \"Аб PairDrop\",\n        \"theme-auto_title\": \"Аўтаматычная адаптацыя тэмы да сістэмы\",\n        \"theme-light_title\": \"Заўсёды выкарыстоўваць светлую тэму\",\n        \"theme-dark_title\": \"Заўсёды выкарыстоўваць цёмную тэму\",\n        \"notification_title\": \"Уключыць апавяшчэнні\",\n        \"edit-paired-devices_title\": \"Рэдагаваць злучаныя прылады\",\n        \"join-public-room_title\": \"Часова далучыцца да публічнага пакоя\",\n        \"cancel-share-mode\": \"Адмяніць\",\n        \"language-selector_title\": \"Задаць мову\",\n        \"install_title\": \"Усталяваць PairDrop\",\n        \"pair-device_title\": \"Злучыце свае прылады назаўжды\",\n        \"edit-share-mode\": \"Рэдагаваць\",\n        \"expand_title\": \"Разгарнуць радок кнопак\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Адпусціце, каб выбраць атрымальніка\",\n        \"no-peers-title\": \"Адкрыйце PairDrop на іншых прыладах, каб адправіць файлы\",\n        \"x-instructions_data-drop-peer\": \"Адпусціце, каб адправіць вузлу\",\n        \"x-instructions_data-drop-bg\": \"Адпусціце, каб выбраць атрымальніка\",\n        \"x-instructions-share-mode_mobile\": \"Дакраніцеся, каб адправіць {{descriptor}}\",\n        \"activate-share-mode-and-other-file\": \"і 1 іньшы файл\",\n        \"activate-share-mode-and-other-files-plural\": \"і {{count}} іньшых файла(ў)\",\n        \"activate-share-mode-shared-text\": \"агульны тэкст\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} агульных файлаў\",\n        \"webrtc-requirement\": \"Каб выкарыстоўваць гэты асобнік Pair Drop, WebRTC павінен быць уключаны!\",\n        \"no-peers-subtitle\": \"Злучыце прылады або ўвайдзіце ў публічны пакой, каб вас маглі выявіць з іншых сетак\",\n        \"x-instructions_mobile\": \"Дакраніцеся, каб адправіць файлы, або доўга трымайце, каб адправіць паведамленне\",\n        \"x-instructions-share-mode_desktop\": \"Націсніце, каб адправіць {{descriptor}}\",\n        \"x-instructions_desktop\": \"Націсніце, каб адправіць файлы, або націсніце правай кнопкай мышы, каб адправіць паведамленне\",\n        \"activate-share-mode-base\": \"Адкрыйце PairDrop на іншых прыладах, каб адправіць\",\n        \"activate-share-mode-shared-file\": \"агульны файл\"\n    },\n    \"footer\": {\n        \"known-as\": \"Вы вядомыя як:\",\n        \"display-name_data-placeholder\": \"Загрузка…\",\n        \"discovery\": \"Вас могуць выявіць:\",\n        \"on-this-network_title\": \"Вас можа знайсці кожны ў гэтай сетцы.\",\n        \"paired-devices\": \"з дапамогай злучаных прылад\",\n        \"public-room-devices_title\": \"Вас могуць выявіць прылады ў гэтай публічнай пакоі незалежна ад сеткі.\",\n        \"traffic\": \"Рух\",\n        \"display-name_title\": \"Зменіце назву сваёй прылады назаўжды\",\n        \"on-this-network\": \"у гэтай сетцы\",\n        \"paired-devices_title\": \"Злучаныя прылады заўсёды могуць вас выявіць незалежна ад сеткі.\",\n        \"public-room-devices\": \"у пакоі {{roomId}}\",\n        \"webrtc\": \", калі WebRTC недаступны.\",\n        \"routed\": \"накіроўваецца праз сервер\"\n    },\n    \"dialogs\": {\n        \"hr-or\": \"АБО\",\n        \"cancel\": \"Адмяніць\",\n        \"pair\": \"Злучыць\",\n        \"unpair\": \"Разлучыць\",\n        \"paired-devices-wrapper_data-empty\": \"Няма злучаных прылад.\",\n        \"auto-accept\": \"аўтаматычнае прыняцце\",\n        \"close\": \"Закрыць\",\n        \"join\": \"Далучыцца\",\n        \"leave\": \"Пакінуць\",\n        \"decline\": \"Адмовіць\",\n        \"share\": \"Падзяліцца\",\n        \"copy\": \"Капіяваць\",\n        \"title-image\": \"Малюнак\",\n        \"system-language\": \"Мова сістэмы\",\n        \"public-room-qr-code_title\": \"Націсніце, каб скапіяваць спасылку на публічны пакой\",\n        \"title-file\": \"Файл\",\n        \"title-file-plural\": \"Файлы\",\n        \"message_placeholder\": \"Тэкст\",\n        \"language-selector-title\": \"Задаць мову\",\n        \"send-message-title\": \"Адправіць паведамленне\",\n        \"scan-qr-code\": \"або сканаваць QR-код.\",\n        \"enter-room-id-from-another-device\": \"Увядзіце ID пакоя з іншай прылады, каб далучыцца да пакоя.\",\n        \"edit-paired-devices-title\": \"Рэдагаваць злучаныя прылады\",\n        \"auto-accept-instructions-1\": \"Актываваць\",\n        \"auto-accept-instructions-2\": \", каб аўтаматычна прымаць усе файлы, адпраўленыя з гэтай прылады.\",\n        \"accept\": \"Прыняць\",\n        \"download\": \"Спампаваць\",\n        \"download-again\": \"Спампаваць яшчэ раз\",\n        \"pair-devices-qr-code_title\": \"Націсніце, каб скапіраваць спасылку для спалучэння гэтай прылады\",\n        \"approve\": \"сцвердзіць\",\n        \"pair-devices-title\": \"Пастаяннае злучэнне прылад\",\n        \"enter-key-from-another-device\": \"Увядзіце тут ключ з іншай прылады.\",\n        \"temporary-public-room-title\": \"Часовы публічны пакой\",\n        \"input-room-id-on-another-device\": \"Увядзіце гэты ID пакоя на іншай прыладзе\",\n        \"paired-device-removed\": \"Злучаная прылада была выдалена.\",\n        \"would-like-to-share\": \"хацеў бы падзяліцца\",\n        \"send-message-to\": \"Каму:\",\n        \"message_title\": \"Устаўце паведамленне для адпраўкі\",\n        \"has-sent\": \"адправіў:\",\n        \"base64-processing\": \"Апрацоўка…\",\n        \"send\": \"Адправіць\",\n        \"base64-title-text\": \"Падзяліцца тэкстам\",\n        \"base64-title-files\": \"Падзяліцца файламі\",\n        \"base64-tap-to-paste\": \"Дакраніцеся тут, каб падзяліцца {{type}}\",\n        \"file-other-description-image\": \"і 1 іньшы малюнак\",\n        \"file-other-description-image-plural\": \"і {{count}} іншых малюнкаў\",\n        \"file-other-description-file-plural\": \"і {{count}} іншых файлаў\",\n        \"title-image-plural\": \"Малюнкі\",\n        \"share-text-subtitle\": \"Рэдагаваць паведамленне перад адпраўкай:\",\n        \"share-text-title\": \"Падзяліцца тэкставым паведамленнем\",\n        \"close-toast_title\": \"Закрыць апавяшчэнне\",\n        \"receive-text-title\": \"Паведамленне атрымана\",\n        \"input-key-on-this-device\": \"Увядзіце гэты ключ на іншай прыладзе\",\n        \"base64-files\": \"файлы\",\n        \"base64-text\": \"тэкст\",\n        \"base64-paste-to-send\": \"Устаўце сюды буфер абмену, каб падзяліцца {{type}}\",\n        \"file-other-description-file\": \"і 1 іньшы файл\",\n        \"receive-title\": \"{{descriptor}} атрымана\",\n        \"share-text-checkbox\": \"Заўсёды паказваць гэта дыялогавае акно пры абагульванні тэксту\"\n    },\n    \"about\": {\n        \"buy-me-a-coffee_title\": \"Купіць мне кавы!\",\n        \"mastodon_title\": \"Напішыце пра PairDrop на Mastodon\",\n        \"tweet_title\": \"Твіт пра PairDrop\",\n        \"github_title\": \"PairDrop на GitHub\",\n        \"custom_title\": \"Сачыце за намі\",\n        \"bluesky_title\": \"Сачыце за намі на BlueSky\",\n        \"faq_title\": \"Часта задаюць пытанні\",\n        \"close-about_aria-label\": \"Закрыць Аб PairDrop\",\n        \"claim\": \"Самы просты спосаб перадачы файлаў паміж прыладамі\",\n        \"privacypolicy_title\": \"Адкрыйце нашу палітыку прыватнасці\"\n    },\n    \"notifications\": {\n        \"link-received\": \"Спасылка атрымана {{name}} - Націсніце, каб адкрыць\",\n        \"message-received\": \"Паведамленне атрымана {{name}} - Націсніце, каб скапіяваць\",\n        \"click-to-download\": \"Націсніце, каб спампаваць\",\n        \"click-to-show\": \"Націсніце, каб паказаць\",\n        \"copied-text-error\": \"Памылка запісу ў буфер абмену. Скапіруйце ўручную!\",\n        \"online\": \"Вы зноў у сетцы\",\n        \"online-requirement-public-room\": \"Вы павінны быць падлучаныя да сеткі, каб стварыць агульны пакой\",\n        \"connecting\": \"Падключэнне…\",\n        \"public-room-id-invalid\": \"Несапраўдны ID пакоя\",\n        \"notifications-permissions-error\": \"Дазвол на апавяшчэнні быў заблакіраваны, бо карыстальнік некалькі разоў адхіляў запыт на дазвол. Гэта можна скінуць у меню \\\"Аб старонцы\\\", доступ да якой можна атрымаць, націснуўшы на значок замка побач з радком URL.\",\n        \"request-title\": \"{{name}} хоча перадаць {{count}} {{descriptor}}\",\n        \"copied-text\": \"Тэкст скапіраваны ў буфер абмену\",\n        \"offline\": \"Вы па-за сеткай\",\n        \"connected\": \"Падключана\",\n        \"online-requirement-pairing\": \"Вы павінны быць падлучаныя да сеткі для спалучэння прылад\",\n        \"pairing-key-invalidated\": \"Ключ {{key}} несапраўдны\",\n        \"display-name-random-again\": \"Адлюстраванае імя зноў згенеравалася выпадковым чынам\",\n        \"download-successful\": \"{{descriptor}} спампавана\",\n        \"pairing-tabs-error\": \"Злучэнне дзвюх укладак вэб-браўзера немагчыма\",\n        \"display-name-changed-permanently\": \"Адлюстроўванае імя было зменена назаўжды\",\n        \"pairing-cleared\": \"Усе прылады раз'яднаны\",\n        \"room-url-copied-to-clipboard\": \"Спасылка на публічны пакой скапіравана ў буфер абмену\",\n        \"public-room-left\": \"Пакінуць публічны пакой {{publicRoomId}}\",\n        \"text-content-incorrect\": \"Змест тэксту няправільны\",\n        \"clipboard-content-incorrect\": \"Змест буфера абмену няправільны\",\n        \"notifications-enabled\": \"Апавяшчэнні ўключаны\",\n        \"files-incorrect\": \"Няправільныя файлы\",\n        \"file-transfer-completed\": \"Перадача файла завершана\",\n        \"selected-peer-left\": \"Выбраны вузел выйшаў\",\n        \"copied-to-clipboard\": \"Скапіравана ў буфер абмену\",\n        \"pair-url-copied-to-clipboard\": \"Спасылка для злучэння гэтай прылады скапіравана ў буфер абмену\",\n        \"pairing-success\": \"Злучаныя прылады\",\n        \"copied-to-clipboard-error\": \"Капіраванне немагчымае. Скапіруйце ўручную.\",\n        \"file-content-incorrect\": \"Змест файла няправільны\",\n        \"pairing-not-persistent\": \"Злучаныя прылады не з'яўляюцца пастаяннымі\",\n        \"pairing-key-invalid\": \"Несапраўдны ключ\",\n        \"display-name-changed-temporarily\": \"Адлюстраванае імя зменена толькі для гэтага сеансу\",\n        \"ios-memory-limit\": \"Адначасовая адпраўка файлаў на iOS магчымая толькі да 200 МБ\",\n        \"message-transfer-completed\": \"Перадача паведамлення завершана\",\n        \"unfinished-transfers-warning\": \"Ёсць незавершаныя перадачы. Вы ўпэўнены, што хочаце закрыць PairDrop?\",\n        \"rate-limit-join-key\": \"Ліміт хуткасці дасягнуты. Пачакайце 10 секунд і паўтарыце спробу.\"\n    },\n    \"peer-ui\": {\n        \"preparing\": \"Падрыхтоўка…\",\n        \"waiting\": \"Чаканне…\",\n        \"transferring\": \"Перадача…\",\n        \"processing\": \"Апрацоўка…\",\n        \"click-to-send-share-mode\": \"Націсніце, каб адправіць {{descriptor}}\",\n        \"connection-hash\": \"Каб праверыць бяспеку скразнога шыфравання, параўнайце гэты нумар бяспекі на абедзвюх прыладах\",\n        \"click-to-send\": \"Націсніце, каб адправіць файлы, або націсніце правай кнопкай мышы, каб адправіць паведамленне\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Файл атрыманы\",\n        \"image-transfer-requested\": \"Запытана перадача малюнкаў\",\n        \"message-received\": \"Паведамленне атрымана\",\n        \"message-received-plural\": \"Атрымана {{count}} паведамленняў\",\n        \"file-received-plural\": \"Атрымана {{count}} файлаў\",\n        \"file-transfer-requested\": \"Запытана перадача файла\"\n    }\n}\n"
  },
  {
    "path": "public/lang/bg.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"Относно PairDrop\",\n        \"language-selector_title\": \"Задай език\",\n        \"about_aria-label\": \"Отвори Относно PairDrop\",\n        \"theme-auto_title\": \"Адаптирай темата спрямо системните настройки\",\n        \"theme-light_title\": \"Винаги използвай светла тема\",\n        \"notification_title\": \"Включи известията\",\n        \"pair-device_title\": \"Свържи устройствата си перманентно\",\n        \"join-public-room_title\": \"Присъедини се към временна публична стая\",\n        \"cancel-share-mode\": \"Отказ\",\n        \"expand_title\": \"Покажи меню\",\n        \"theme-dark_title\": \"Винаги използвай тъмна тема\",\n        \"install_title\": \"Инсталирай PairDrop\",\n        \"edit-paired-devices_title\": \"Промени свързаните устройства\",\n        \"edit-share-mode\": \"Промени\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Пусни, за да избереш получател\",\n        \"no-peers-title\": \"Отвори PairDrop на друго устройство, за да започнеш споделяне\",\n        \"no-peers-subtitle\": \"Свържи устройство или влез в публична стая, за да станеш откриваем за други мрежи\",\n        \"x-instructions_desktop\": \"Ляв клик, за да изпратиш файл или десен клик, за да изпратиш съобщение\",\n        \"x-instructions_data-drop-peer\": \"Пусни, за да изпратиш\",\n        \"x-instructions-share-mode_desktop\": \"Натисни, за да изпратиш {{descriptor}}\",\n        \"activate-share-mode-base\": \"Отвори PairDrop на друго устройство, за да изпратиш\",\n        \"activate-share-mode-and-other-file\": \"и още 1 файл\",\n        \"activate-share-mode-and-other-files-plural\": \"и още {{count}} файла\",\n        \"activate-share-mode-shared-text\": \"споделен текст\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} споделени файлове\",\n        \"x-instructions_data-drop-bg\": \"Пусни, за да избереш получател\",\n        \"activate-share-mode-shared-file\": \"споделен файл\",\n        \"x-instructions_mobile\": \"Докосни, за да изпратиш файл или задръж, за да изпратиш съобщение\",\n        \"x-instructions-share-mode_mobile\": \"Докосни, за да изпратиш {{descriptor}}\",\n        \"webrtc-requirement\": \"За да използвате инстанция на PairDrop, WebRTC трябва да бъде включен!\"\n    },\n    \"footer\": {\n        \"known-as\": \"Известни сте като:\",\n        \"display-name_data-placeholder\": \"Зареждане…\",\n        \"display-name_title\": \"Редактирайте името на вашето устройство за постоянно\",\n        \"discovery\": \"Може да бъдете открити:\",\n        \"on-this-network\": \"В тази мрежа\",\n        \"on-this-network_title\": \"Може да бъдете открити от всеки в тази мрежа.\",\n        \"paired-devices\": \"От свързани устройства\",\n        \"paired-devices_title\": \"Може да бъдете открити от свързани устройства по всяко време, независимо от мрежата.\",\n        \"public-room-devices\": \"в стая {{roomId}}\",\n        \"public-room-devices_title\": \"Може да бъдете открити от устройства в тази публична стая, независимо от мрежата.\",\n        \"traffic\": \"Трафикът е\",\n        \"routed\": \"пренасочен през сървъра\",\n        \"webrtc\": \"ако WebRTC не е наличен.\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"Свържете устройства за постоянно\",\n        \"input-key-on-this-device\": \"Въведете този ключ на друго устройство\",\n        \"scan-qr-code\": \"или сканирайте QR кода.\",\n        \"enter-key-from-another-device\": \"Въведете тук ключ от друго устройство.\",\n        \"input-room-id-on-another-device\": \"Въведете този ID за стая на друго устройство\",\n        \"hr-or\": \"ИЛИ\",\n        \"unpair\": \"Раздели\",\n        \"paired-device-removed\": \"Свързаното устройство беше премахнато.\",\n        \"paired-devices-wrapper_data-empty\": \"Няма свързани устройства.\",\n        \"auto-accept-instructions-1\": \"Активно\",\n        \"auto-accept\": \"автоматично приемане\",\n        \"auto-accept-instructions-2\": \"за автоматично приемане на всички файлове, изпратени от това устройство.\",\n        \"close\": \"Затвори\",\n        \"join\": \"Присъедини се\",\n        \"leave\": \"Напусни\",\n        \"would-like-to-share\": \"иска да сподели\",\n        \"has-sent\": \"изпрати:\",\n        \"share\": \"Сподели\",\n        \"download\": \"Изтегли\",\n        \"send-message-title\": \"Изпрати съобщение\",\n        \"send-message-to\": \"До:\",\n        \"message_title\": \"Въведете съобщението за изпращане\",\n        \"message_placeholder\": \"Текст\",\n        \"send\": \"Изпрати\",\n        \"receive-text-title\": \"Получено съобщение\",\n        \"copy\": \"Копиране\",\n        \"base64-title-files\": \"Споделяне на файлове\",\n        \"base64-title-text\": \"Споделяне на текст\",\n        \"base64-processing\": \"Обработва се…\",\n        \"base64-tap-to-paste\": \"Натиснете тук за да споделите {{type}}\",\n        \"base64-paste-to-send\": \"Поставете своя Клипборд тук за да споделите {{type}}\",\n        \"file-other-description-image\": \"и още една снимка\",\n        \"file-other-description-file\": \"и още един файл\",\n        \"file-other-description-image-plural\": \"и още {{count}} снимки\",\n        \"file-other-description-file-plural\": \"и още {{count}} файла\",\n        \"title-image\": \"снимка\",\n        \"title-image-plural\": \"Снимки\",\n        \"title-file-plural\": \"Файлове\",\n        \"receive-title\": \"{{descriptor}} Получено\",\n        \"download-again\": \"Изтегли отново\",\n        \"language-selector-title\": \"Изберете език\",\n        \"pair-devices-qr-code_title\": \"Кликнете, за да копирате линка за свързване на това устройство\",\n        \"approve\": \"одобри\",\n        \"share-text-title\": \"Сподели текстово съобщение\",\n        \"share-text-subtitle\": \"Редактирайте съобщението преди изпращане:\",\n        \"share-text-checkbox\": \"Винаги показвай този прозорец при споделяне на текст\",\n        \"accept\": \"Приеми\",\n        \"temporary-public-room-title\": \"Временна публична стая\",\n        \"enter-room-id-from-another-device\": \"Въведете ID на стая от друго устройство, за да се присъедините.\",\n        \"pair\": \"Свържи\",\n        \"cancel\": \"Отказ\",\n        \"edit-paired-devices-title\": \"Редактиране на свързани устройства\",\n        \"close-toast_title\": \"Затвори известието\",\n        \"decline\": \"Откажи\",\n        \"base64-text\": \"текст\",\n        \"base64-files\": \"файлове\",\n        \"title-file\": \"Файл\",\n        \"system-language\": \"Език на системата\",\n        \"public-room-qr-code_title\": \"Кликнете, за да копирате линка към публичната стая\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"Затвори информацията за PairDrop\",\n        \"tweet_title\": \"Споделете PairDrop в X (Twitter)\",\n        \"mastodon_title\": \"Напишете за PairDrop в Mastodon\",\n        \"bluesky_title\": \"Последвайте ни в BlueSky\",\n        \"custom_title\": \"Последвайте ни\",\n        \"faq_title\": \"Често задавани въпроси\",\n        \"claim\": \"Най-лесният начин за прехвърляне на файлове между устройства\",\n        \"github_title\": \"PairDrop в GitHub\",\n        \"buy-me-a-coffee_title\": \"Купете ми кафе!\",\n        \"privacypolicy_title\": \"Отворете нашата политика за поверителност\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"Името се променя постоянно\",\n        \"display-name-changed-temporarily\": \"Името се променя само за тази сесия\",\n        \"display-name-random-again\": \"Името отново е генерирано на случаен принцип\",\n        \"pairing-not-persistent\": \"Свързаните устройства не са постоянни\",\n        \"public-room-left\": \"Напуснахте публичната стая {{publicRoomId}}\",\n        \"copied-to-clipboard\": \"Копирано в клипборда\",\n        \"pair-url-copied-to-clipboard\": \"Линкът за свързване на това устройство е копиран в клипборда\",\n        \"room-url-copied-to-clipboard\": \"Линкът към публичната стая е копиран в клипборда\",\n        \"copied-to-clipboard-error\": \"Копирането не е възможно. Копирайте ръчно.\",\n        \"file-content-incorrect\": \"Съдържанието на файла е неправилно\",\n        \"clipboard-content-incorrect\": \"Съдържанието на клипборда е неправилно\",\n        \"link-received\": \"Линк, получен от {{name}} - Кликнете, за да отворите\",\n        \"message-received\": \"Съобщение, получено от {{name}} - Кликнете, за да копирате\",\n        \"click-to-download\": \"Кликнете, за да изтеглите\",\n        \"request-title\": \"{{name}} иска да прехвърли {{count}} {{descriptor}}\",\n        \"click-to-show\": \"Кликнете, за да покажете\",\n        \"copied-text\": \"Текстът е копиран в клипборда\",\n        \"copied-text-error\": \"Писането в клипборда не успя. Копирайте ръчно!\",\n        \"offline\": \"Вие сте офлайн\",\n        \"online\": \"Вие сте отново онлайн\",\n        \"connected\": \"Свързан\",\n        \"online-requirement-public-room\": \"Трябва да сте онлайн, за да създадете публична стая\",\n        \"files-incorrect\": \"Файловете са неправилни\",\n        \"unfinished-transfers-warning\": \"Има незавършени прехвърляния. Сигурни ли сте, че искате да затворите PairDrop?\",\n        \"selected-peer-left\": \"Избраният партньор напусна\",\n        \"pairing-tabs-error\": \"Свързването на два раздела в браузъра е невъзможно\",\n        \"pairing-success\": \"Устройства свързани\",\n        \"download-successful\": \"{{descriptor}} изтеглено\",\n        \"public-room-id-invalid\": \"Невалиден ID на стая\",\n        \"pairing-key-invalid\": \"Невалиден ключ\",\n        \"pairing-cleared\": \"Всички устройства са разделени\",\n        \"text-content-incorrect\": \"Текстовото съдържание е неправилно\",\n        \"notifications-enabled\": \"Известията са активирани\",\n        \"pairing-key-invalidated\": \"Ключът {{key}} е невалиден\",\n        \"notifications-permissions-error\": \"Разрешението за известия е блокирано, тъй като потребителят няколко пъти е отхвърлил подкана за разрешение. Това може да се нулира в информацията за страницата, която се достъпва чрез иконата с катинар до лентата за URL адрес.\",\n        \"online-requirement-pairing\": \"Трябва да сте онлайн, за да свържете устройства\",\n        \"connecting\": \"Свързване…\",\n        \"file-transfer-completed\": \"Прехвърлянето на файлове е завършено\",\n        \"rate-limit-join-key\": \"Достигнат е лимитът за заявки. Изчакайте 10 секунди и опитайте отново.\",\n        \"message-transfer-completed\": \"Прехвърлянето на съобщението е завършено\",\n        \"ios-memory-limit\": \"Изпращането на файлове към iOS е възможно само до 200 MB наведнъж\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Файлът е получен\",\n        \"file-received-plural\": \"{{count}} файла получени\",\n        \"file-transfer-requested\": \"Заявката за прехвърляне на файлове е изпратена\",\n        \"image-transfer-requested\": \"Заявката за прехвърляне на изображения е изпратена\",\n        \"message-received-plural\": \"{{count}} получени съобщения\",\n        \"message-received\": \"Получено съобщение\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Натисни, за да изпратиш {{descriptor}}\",\n        \"preparing\": \"Подготовка…\",\n        \"click-to-send\": \"Кликнете, за да изпратите файлове или кликнете с десен бутон, за да изпратите съобщение\",\n        \"connection-hash\": \"За да потвърдите сигурността на криптирането на връзката, сравнете този номер за сигурност с двете устройства\",\n        \"waiting\": \"Чакане…\",\n        \"processing\": \"Обработка…\",\n        \"transferring\": \"Прехвърляне…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/bn.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"পেয়ার ড্রপ সম্পর্কে\",\n        \"install_title\": \"পেয়ার ড্রপ ইন্সটল করুন\",\n        \"pair-device_title\": \"ডিভাইস স্থায়ী ভাবে যুক্ত করুন\",\n        \"cancel-share-mode\": \"বাতিল\",\n        \"theme-light_title\": \"সবসময় সাদা থিম ব্যাবহার\",\n        \"language-selector_title\": \"ভাষা সেট করুন\",\n        \"about_aria-label\": \"পেয়ারড্রপ সম্পর্কে\",\n        \"theme-auto_title\": \"থিমের ধরন ডিভাইস অনুযায়ী\",\n        \"theme-dark_title\": \"সবসময় কালো থিব ব্যাবহার\",\n        \"notification_title\": \"নোটিফিকেশন চালু করুন\",\n        \"edit-paired-devices_title\": \"যুক্ত ডিভাইস সম্পাদনা করুন\",\n        \"join-public-room_title\": \"সাময়িক ভাবে পাবলিক রুমে জয়েন করুন\",\n        \"edit-share-mode\": \"সম্পাদনা\",\n        \"expand_title\": \"হেডার বোতামের সারিটি বড় করুন\"\n    },\n    \"instructions\": {\n        \"activate-share-mode-and-other-file\": \"আর একটি ফাইল যোগ করুন\",\n        \"activate-share-mode-shared-file\": \"পাঠানো ফাইল\",\n        \"no-peers-subtitle\": \"ডিভাইস প্রদর্শিত হতে নতুন ডিভাইস যুক্ত করুন অথবা পাবলিক রুমে জয়েন দিন\",\n        \"no-peers-title\": \"ফাইল পাঠানোর জন্য অন্যান্য ডিভাইসে পেয়ারড্রপ খুলুন\",\n        \"x-instructions_data-drop-bg\": \"প্রাপক নির্বাচন করতে ছেড়ে দিন\",\n        \"no-peers_data-drop-bg\": \"প্রাপক নির্বাচন ছেড়ে দিন\",\n        \"x-instructions_desktop\": \"ফাইল পাঠাতে ক্লিক করুন অথবা মেসেজ পাঠাতে ডানে চাপুন\",\n        \"x-instructions_mobile\": \"ফাইল পাঠাতে ক্লিক করুন অথবা বেশি চেপে মেসেজ পাঠান\",\n        \"x-instructions_data-drop-peer\": \"পিয়ারকে পাঠানোর জন্য রিলিজ করুন\",\n        \"x-instructions-share-mode_desktop\": \"পাঠাতে ক্লিক করুন\",\n        \"x-instructions-share-mode_mobile\": \"পাঠাতে ক্লিক করুন\",\n        \"activate-share-mode-base\": \"অন্য ডিভাইসে পাঠাতে পেয়ারড্রপ খুলুন\",\n        \"activate-share-mode-and-other-files-plural\": \"অন্য ফাইল যোগ করুন\",\n        \"activate-share-mode-shared-text\": \"পাঠানো টেক্সট\",\n        \"activate-share-mode-shared-files-plural\": \"পাঠানো ফাইল গুলো\"\n    }\n}\n"
  },
  {
    "path": "public/lang/ca.json",
    "content": "{\n    \"instructions\": {\n        \"x-instructions_mobile\": \"Toca per enviar fitxers o mantén premut per enviar un missatge\",\n        \"no-peers-subtitle\": \"Vincula dispositius o entra a una sala pública per ser detectable en altres xarxes\",\n        \"x-instructions_desktop\": \"Fes click per enviar fitxers o click dret per enviar un missatge de text\",\n        \"no-peers-title\": \"Obre PairDrop a altres dispositius per enviar arxius\",\n        \"x-instructions_data-drop-peer\": \"Allibera per enviar a un company\",\n        \"x-instructions_data-drop-bg\": \"Allibera per seleccionar recipient\",\n        \"no-peers_data-drop-bg\": \"Allibera per seleccionar destinatari\",\n        \"activate-share-mode-base\": \"Obre PairDrop a altres dispositius per enviar\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} fitxers compartits\",\n        \"x-instructions-share-mode_desktop\": \"Clica per enviar {{descriptor}}\",\n        \"activate-share-mode-shared-file\": \"fitxer compartit\",\n        \"activate-share-mode-and-other-file\": \"i 1 altre fitxer\",\n        \"x-instructions-share-mode_mobile\": \"Toca per enviar {{descriptor}}\",\n        \"activate-share-mode-and-other-files-plural\": \"i {{count}} fitxers més\",\n        \"activate-share-mode-shared-text\": \"text compartit\",\n        \"webrtc-requirement\": \"Per utilitzar aquesta instància de PairDrop cal habilitar WebRTC!\"\n    },\n    \"header\": {\n        \"theme-auto_title\": \"Adapta el tema al del sistema automàticament\",\n        \"install_title\": \"Instal·la PairDrop\",\n        \"theme-dark_title\": \"Utilitza sempre el mode fosc\",\n        \"pair-device_title\": \"Vincula els teus dispositius permanentment\",\n        \"join-public-room_title\": \"Uneix-te temporalment a una sala pública\",\n        \"notification_title\": \"Permet les notificacions\",\n        \"edit-paired-devices_title\": \"Edita els dispositius vinculats\",\n        \"edit-share-mode\": \"Editar\",\n        \"language-selector_title\": \"Configurar idioma\",\n        \"cancel-share-mode\": \"Cancel·lar\",\n        \"about_title\": \"Sobre PairDrop\",\n        \"about_aria-label\": \"Obre Sobre PairDrop\",\n        \"theme-light_title\": \"Utilitza sempre el mode clar\",\n        \"expand_title\": \"Expandeix la fila de botons de la capçalera\"\n    },\n    \"dialogs\": {\n        \"message_placeholder\": \"Text\",\n        \"base64-paste-to-send\": \"Enganxa el contingut del porta-retalls aquí per compartir {{type}}\",\n        \"auto-accept-instructions-2\": \"per acceptar automàticament tots els fitxers enviats des d'aquell dispositiu.\",\n        \"receive-text-title\": \"Missatge Rebut\",\n        \"edit-paired-devices-title\": \"Edita els Dispositius Vinculats\",\n        \"cancel\": \"Cancel·lar\",\n        \"auto-accept-instructions-1\": \"Activar\",\n        \"pair-devices-title\": \"Vincula Dispositius Permanentment\",\n        \"download\": \"Descarregar\",\n        \"title-file\": \"Fitxer\",\n        \"close-toast_title\": \"Tanca notificació\",\n        \"base64-processing\": \"Processant…\",\n        \"decline\": \"Rebutjar\",\n        \"receive-title\": \"{{descriptor}} Rebut\",\n        \"share-text-checkbox\": \"Mostra sempre aquesta finestra de diàleg en compartir text\",\n        \"leave\": \"Marxar\",\n        \"message_title\": \"Introdueix el missatge a enviar\",\n        \"join\": \"Unir-se\",\n        \"title-image-plural\": \"Imatges\",\n        \"send\": \"Enviar\",\n        \"base64-title-files\": \"Compartir Fitxers\",\n        \"base64-tap-to-paste\": \"Toca aquí per compartir {{type}}\",\n        \"base64-text\": \"text\",\n        \"copy\": \"Copiar\",\n        \"file-other-description-image\": \"i 1 altra imatge\",\n        \"pair-devices-qr-code_title\": \"Clica per copiar l'enllaç per vincular aquest dispositiu\",\n        \"approve\": \"aprovar\",\n        \"temporary-public-room-title\": \"Sala Pública Temporal\",\n        \"base64-files\": \"fitxers\",\n        \"paired-device-removed\": \"Dispositiu vinculat eliminat.\",\n        \"has-sent\": \"ha enviat:\",\n        \"share-text-title\": \"Compartir Missatge de Text\",\n        \"file-other-description-file\": \"i 1 altre fitxer\",\n        \"public-room-qr-code_title\": \"Clica per copiar l'enllaç a la sala pública\",\n        \"close\": \"Tancar\",\n        \"system-language\": \"Idioma del Sistema\",\n        \"share-text-subtitle\": \"Edita el missatge abans d'enviar:\",\n        \"unpair\": \"Desvincular\",\n        \"title-image\": \"Imatge\",\n        \"file-other-description-file-plural\": \"i {{count}} altres fitxers\",\n        \"would-like-to-share\": \"voldria compartir\",\n        \"base64-title-text\": \"Compartir Text\",\n        \"send-message-to\": \"Per a:\",\n        \"language-selector-title\": \"Establir idioma\",\n        \"pair\": \"Vincular\",\n        \"hr-or\": \"O\",\n        \"scan-qr-code\": \"o escaneja el codi QR.\",\n        \"input-key-on-this-device\": \"Introdueix aquesta clau a un altre dispositiu\",\n        \"download-again\": \"Descarregar de nou\",\n        \"accept\": \"Acceptar\",\n        \"paired-devices-wrapper_data-empty\": \"No hi ha dispositius vinculats.\",\n        \"enter-key-from-another-device\": \"Introdueix la clau d'un altre dispositiu aquí.\",\n        \"share\": \"Compartir\",\n        \"auto-accept\": \"auto-acceptar\",\n        \"title-file-plural\": \"Fitxers\",\n        \"send-message-title\": \"Enviar Missatge\",\n        \"input-room-id-on-another-device\": \"Introdueix aquest ID de sala a un altre dispositiu\",\n        \"file-other-description-image-plural\": \"i {{count}} altres imatges\",\n        \"enter-room-id-from-another-device\": \"Introdueix l'ID de sala d'un altre dispositiu per unir-t'hi.\"\n    },\n    \"footer\": {\n        \"webrtc\": \"si WebRTC no està disponible.\",\n        \"public-room-devices_title\": \"Pots ser descobert per dispositius en aquesta sala pública independentment de la xarxa.\",\n        \"display-name_data-placeholder\": \"Carregant…\",\n        \"display-name_title\": \"Edita el nom del teu dispositiu permanentment\",\n        \"traffic\": \"El trànsit és\",\n        \"paired-devices_title\": \"Pots ser descobert per dispositius emparellats en qualsevol moment, independentment de la xarxa.\",\n        \"public-room-devices\": \"a la sala {{roomId}}\",\n        \"paired-devices\": \"per dispositius vinculats\",\n        \"on-this-network\": \"En aquesta xarxa\",\n        \"routed\": \"encaminat a través del servidor\",\n        \"discovery\": \"Pots ser descobert:\",\n        \"on-this-network_title\": \"Pots ser descobert per qualsevol usuari en aquesta xarxa.\",\n        \"known-as\": \"Ets conegut com a:\"\n    },\n    \"notifications\": {\n        \"request-title\": \"{{name}} voldria transferir {{count}} {{descriptor}}\",\n        \"unfinished-transfers-warning\": \"Hi ha transferències pendents. Estàs segur que vols tancar PairDrop?\",\n        \"message-received\": \"Missatge rebut per {{name}} - Fes clic per copiar\",\n        \"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.\",\n        \"rate-limit-join-key\": \"S'ha arribat al límit de ràtio. Espera 10 segons i torna-ho a intentar.\",\n        \"pair-url-copied-to-clipboard\": \"Enllaç per vincular aquest dispositiu copiat al porta-retalls\",\n        \"connecting\": \"Connectant…\",\n        \"pairing-key-invalidated\": \"Clau {{key}} invalidada\",\n        \"pairing-key-invalid\": \"Clau no vàlida\",\n        \"connected\": \"Connectat\",\n        \"pairing-not-persistent\": \"Els dispositius vinculats no són persistents\",\n        \"text-content-incorrect\": \"El contingut del text és incorrecte\",\n        \"message-transfer-completed\": \"Transferència de missatge completada\",\n        \"file-transfer-completed\": \"Transferència de fitxers completada\",\n        \"file-content-incorrect\": \"El contingut del fitxer és incorrecte\",\n        \"files-incorrect\": \"Els fitxers són incorrectes\",\n        \"selected-peer-left\": \"L'usuari seleccionat ha marxat\",\n        \"link-received\": \"Enllaç rebut per {{name}} - Fes clic per obrir\",\n        \"online\": \"Tornes a estar en línia\",\n        \"public-room-left\": \"Has sortit de la sala pública {{publicRoomId}}\",\n        \"copied-text\": \"Text copiat al porta-retalls\",\n        \"display-name-random-again\": \"El nom d'usuari ha estat generat novament generat aleatòriament\",\n        \"display-name-changed-permanently\": \"El nom d'usuari està canviat permanentment\",\n        \"copied-to-clipboard-error\": \"Còpia impossible. Copiar manualment.\",\n        \"pairing-success\": \"Dispositius vinculats\",\n        \"clipboard-content-incorrect\": \"El contingut del porta-retalls és incorrecte\",\n        \"display-name-changed-temporarily\": \"El nom d'usuari està canviat només per aquesta sessió\",\n        \"copied-to-clipboard\": \"Copiat al porta-retalls\",\n        \"offline\": \"No estàs en línia\",\n        \"pairing-tabs-error\": \"No es poden vincular dues pestanyes d'un navegador\",\n        \"public-room-id-invalid\": \"ID de sala no vàlid\",\n        \"click-to-download\": \"Clica per descarregar\",\n        \"pairing-cleared\": \"Tots els dispositius desvinculats\",\n        \"notifications-enabled\": \"Notificacions habilitades\",\n        \"online-requirement-pairing\": \"Has d'estar en línia per vincular dispositius\",\n        \"ios-memory-limit\": \"Tan sols és possible enviar fitxers de fins a 200 MB a iOS\",\n        \"online-requirement-public-room\": \"Cal que estiguis en línia per poder crear una sala pública\",\n        \"room-url-copied-to-clipboard\": \"Enllaç a la sala pública copiat al porta-retalls\",\n        \"copied-text-error\": \"L'escriptura al porta-retalls ha fallat. Copiar manualment!\",\n        \"download-successful\": \"{{descriptor}} descarregat\",\n        \"click-to-show\": \"Clica per mostrar\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"Processant…\",\n        \"click-to-send-share-mode\": \"Clica per enviar {{descriptor}}\",\n        \"click-to-send\": \"Fes clic per enviar fitxers o fes clic dret per enviar un missatge\",\n        \"waiting\": \"Esperant…\",\n        \"connection-hash\": \"Per verificar la seguretat del xifratge de punta a punta, compara aquest número de seguretat en ambdós dispositius\",\n        \"preparing\": \"Preparant…\",\n        \"transferring\": \"Transferint…\"\n    },\n    \"about\": {\n        \"claim\": \"La manera més fàcil de compartir fitxers entre dispositius\",\n        \"tweet_title\": \"Tuiteja sobre PairDrop\",\n        \"close-about_aria-label\": \"Tanca Sobre PairDrop\",\n        \"buy-me-a-coffee_title\": \"Convida'm a un cafè!\",\n        \"github_title\": \"PairDrop a GitHub\",\n        \"faq_title\": \"Preguntes freqüents\",\n        \"mastodon_title\": \"Escriu sobre PairDrop a Mastodon\",\n        \"bluesky_title\": \"Segueix-nos a BlueSky\",\n        \"custom_title\": \"Segueix-nos\",\n        \"privacypolicy_title\": \"Obre la nostra política de privacitat\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"Transferència de Fitxers Sol·licitada\",\n        \"image-transfer-requested\": \"Transferència d'Imatges Sol·licitada\",\n        \"message-received-plural\": \"{{count}} Missatges Rebuts\",\n        \"message-received\": \"Missatge Rebut\",\n        \"file-received\": \"Fitxer Rebut\",\n        \"file-received-plural\": \"{{count}} Fitxers Rebuts\"\n    }\n}\n"
  },
  {
    "path": "public/lang/cs.json",
    "content": "{\n    \"header\": {\n        \"about_aria-label\": \"Otevřít o PairDrop\",\n        \"about_title\": \"O službě PairDrop\",\n        \"language-selector_title\": \"Nastavit jazyk\",\n        \"theme-auto_title\": \"Automatické přizpůsobení tématu systému\",\n        \"pair-device_title\": \"Spárovat zařízení permanentně\",\n        \"theme-light_title\": \"Vždy používat světlé téma\",\n        \"theme-dark_title\": \"Vždy používat tmavé téma\",\n        \"notification_title\": \"Povolit upozornění\",\n        \"install_title\": \"Nainstalovat PairDrop\",\n        \"edit-paired-devices_title\": \"Upravit spárovaná zařízení\",\n        \"join-public-room_title\": \"Připojte se dočasně k veřejné místnosti\",\n        \"cancel-share-mode\": \"Zrušit\",\n        \"edit-share-mode\": \"Upravit\",\n        \"expand_title\": \"Rozbalit řádek tlačítka záhlaví\"\n    },\n    \"about\": {\n        \"buy-me-a-coffee_title\": \"Kupte mi kávu!\",\n        \"close-about_aria-label\": \"Zavřít O PairDrop\",\n        \"claim\": \"Nejjednodušší způsob přenosu souborů mezi zařízeními\",\n        \"github_title\": \"PairDrop na GitHubu\",\n        \"tweet_title\": \"Tweet o PairDrop\",\n        \"mastodon_title\": \"Napište o PairDrop na Mastodon\",\n        \"custom_title\": \"Sledujte nás\",\n        \"privacypolicy_title\": \"Otevřete naše zásady ochrany osobních údajů\",\n        \"bluesky_title\": \"Sledujte nás na BlueSky\",\n        \"faq_title\": \"Často kladené otázky\"\n    },\n    \"footer\": {\n        \"webrtc\": \"pokud WebRTC není k dispozici.\",\n        \"known-as\": \"Jste známí jako:\",\n        \"display-name_data-placeholder\": \"Načítání…\",\n        \"display-name_title\": \"Trvale upravit název zařízení\",\n        \"discovery\": \"Můžete být objeveni:\",\n        \"on-this-network\": \"na této síti\",\n        \"on-this-network_title\": \"V této síti vás může objevit každý.\",\n        \"paired-devices\": \"pomocí spárovaných zařízení\",\n        \"paired-devices_title\": \"Spárovaná zařízení vás mohou kdykoli objevit nezávisle na síti.\",\n        \"public-room-devices\": \"v místnosti {{roomId}}\",\n        \"public-room-devices_title\": \"Zařízení v této veřejné místnosti vás mohou objevit nezávisle na síti.\",\n        \"traffic\": \"Provoz je\",\n        \"routed\": \"směrován přes server\"\n    },\n    \"dialogs\": {\n        \"auto-accept\": \"auto-accept\",\n        \"pair-devices-title\": \"Spárujte zařízení trvale\",\n        \"input-key-on-this-device\": \"Zadejte tento klíč na jiném zařízení\",\n        \"scan-qr-code\": \"nebo naskenujte QR kód.\",\n        \"enter-key-from-another-device\": \"Zde zadejte klíč z jiného zařízení.\",\n        \"temporary-public-room-title\": \"Dočasná veřejná místnost\",\n        \"input-room-id-on-another-device\": \"Zadejte toto ID místnosti na jiném zařízení\",\n        \"enter-room-id-from-another-device\": \"Chcete-li se připojit k místnosti, zadejte ID místnosti z jiného zařízení.\",\n        \"hr-or\": \"NEBO\",\n        \"pair\": \"Párovat\",\n        \"cancel\": \"Zrušit\",\n        \"edit-paired-devices-title\": \"Upravit spárovaná zařízení\",\n        \"unpair\": \"Zrušit spárování\",\n        \"paired-device-removed\": \"Spárované zařízení bylo odstraněno.\",\n        \"paired-devices-wrapper_data-empty\": \"Žádná spárovaná zařízení.\",\n        \"auto-accept-instructions-1\": \"Aktivací\",\n        \"auto-accept-instructions-2\": \"budete automaticky přijímat všechny soubory odeslané z tohoto zařízení.\",\n        \"close\": \"Zavřít\",\n        \"join\": \"Připojit\",\n        \"leave\": \"Odejít\",\n        \"accept\": \"Přijmout\",\n        \"decline\": \"Odmítnout\",\n        \"would-like-to-share\": \"by se rád podělil\",\n        \"has-sent\": \"odeslal:\",\n        \"share\": \"Sdílet\",\n        \"download\": \"Stáhnout\",\n        \"send-message-title\": \"Poslat zprávu\",\n        \"send-message-to\": \"Komu:\",\n        \"message_title\": \"Vložte zprávu k odeslání\",\n        \"message_placeholder\": \"Text\",\n        \"send\": \"Odeslat\",\n        \"receive-text-title\": \"Zpráva přijata\",\n        \"copy\": \"Kopírovat\",\n        \"base64-title-files\": \"Sdílet soubory\",\n        \"base64-title-text\": \"Sdílet text\",\n        \"base64-processing\": \"Zpracovává se…\",\n        \"base64-tap-to-paste\": \"Klepnutím sem sdílejte {{type}}\",\n        \"base64-files\": \"soubory\",\n        \"file-other-description-image\": \"a 1 další obrázek\",\n        \"base64-paste-to-send\": \"Sem vložte schránku pro sdílení {{type}}\",\n        \"base64-text\": \"text\",\n        \"file-other-description-file\": \"a 1 další soubor\",\n        \"file-other-description-image-plural\": \"a další obrázky ({{count}})\",\n        \"file-other-description-file-plural\": \"a {{count}} dalších souborů\",\n        \"title-image\": \"Obrázek\",\n        \"title-file\": \"Soubor\",\n        \"title-image-plural\": \"Obrázky\",\n        \"title-file-plural\": \"Soubory\",\n        \"receive-title\": \"{{descriptor}} Přijato\",\n        \"download-again\": \"Stáhnout znovu\",\n        \"language-selector-title\": \"Nastavit jazyk\",\n        \"system-language\": \"Jazyk systému\",\n        \"public-room-qr-code_title\": \"Kliknutím zkopírujete odkaz do veřejné místnosti\",\n        \"pair-devices-qr-code_title\": \"Kliknutím zkopírujete odkaz pro spárování tohoto zařízení\",\n        \"approve\": \"schválit\",\n        \"share-text-title\": \"Sdílet textovou zprávu\",\n        \"share-text-subtitle\": \"Upravit zprávu před odesláním:\",\n        \"share-text-checkbox\": \"Při sdílení textu vždy zobrazit tento dialog\",\n        \"close-toast_title\": \"Zavřít oznámení\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Uvolněním vyberte příjemce\",\n        \"no-peers-title\": \"Otevřete PairDrop na jiných zařízeních a posílejte soubory\",\n        \"no-peers-subtitle\": \"Spárujte zařízení nebo vstupte do veřejné místnosti, abyste byli zjistitelní v jiných sítích\",\n        \"x-instructions_desktop\": \"Kliknutím odešlete soubory nebo kliknutím pravým tlačítkem odešlete zprávu\",\n        \"x-instructions_mobile\": \"Klepnutím odešlete soubory nebo dlouhým klepnutím odešlete zprávu\",\n        \"x-instructions_data-drop-peer\": \"Uvolněním odešlete\",\n        \"x-instructions_data-drop-bg\": \"Uvolněním vyberte příjemce\",\n        \"x-instructions-share-mode_desktop\": \"Kliknutím odešlete {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"Klepnutím odešlete {{descriptor}}\",\n        \"activate-share-mode-base\": \"Pro odeslání otevřete PairDrop na jiných zařízeních\",\n        \"activate-share-mode-and-other-file\": \"a 1 další soubor\",\n        \"activate-share-mode-and-other-files-plural\": \"a {{count}} dalších souborů\",\n        \"activate-share-mode-shared-text\": \"sdílený text\",\n        \"activate-share-mode-shared-file\": \"sdílený soubor\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} sdílených souborů\",\n        \"webrtc-requirement\": \"Chcete-li použít PairDrop, musí být povoleno WebRTC!\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"Zobrazované jméno je trvale změněno\",\n        \"display-name-changed-temporarily\": \"Zobrazované jméno je změněno pouze pro tuto relaci\",\n        \"display-name-random-again\": \"Zobrazované jméno je opět náhodně generováno\",\n        \"download-successful\": \"{{descriptor}} staženo\",\n        \"pairing-tabs-error\": \"Spárování dvou záložek webového prohlížeče není možné\",\n        \"pairing-success\": \"Zařízení spárována\",\n        \"pairing-not-persistent\": \"Spárovaná zařízení nejsou trvalá\",\n        \"pairing-key-invalid\": \"Neplatný klíč\",\n        \"pairing-key-invalidated\": \"Klíč {{key}} byl neplatný\",\n        \"public-room-id-invalid\": \"Neplatné ID místnosti\",\n        \"public-room-left\": \"Opuštěna veřejná místnost {{publicRoomId}}\",\n        \"copied-to-clipboard\": \"Zkopírováno do schránky\",\n        \"pair-url-copied-to-clipboard\": \"Odkaz pro spárování tohoto zařízení byl zkopírován do schránky\",\n        \"room-url-copied-to-clipboard\": \"Odkaz do veřejné místnosti zkopírován do schránky\",\n        \"pairing-cleared\": \"Všechna nespárovaná zařízení\",\n        \"copied-to-clipboard-error\": \"Kopírování není možné. Kopírovat ručně.\",\n        \"text-content-incorrect\": \"Textový obsah je nesprávný\",\n        \"file-content-incorrect\": \"Obsah souboru je nesprávný\",\n        \"clipboard-content-incorrect\": \"Obsah schránky je nesprávný\",\n        \"notifications-enabled\": \"Oznámení povolena\",\n        \"notifications-permissions-error\": \"Oprávnění k oznámení bylo zablokováno, protože uživatel několikrát odmítl výzvu k povolení. Toto lze resetovat v části Informace o stránce, ke které se dostanete kliknutím na ikonu zámku vedle řádku adresy URL.\",\n        \"link-received\": \"Odkaz obdržel {{name}} – kliknutím otevřete\",\n        \"message-received\": \"Zpráva přijatá uživatelem {{name}} – kliknutím zkopírujte\",\n        \"click-to-download\": \"Kliknutím stáhnete\",\n        \"request-title\": \"{{name}} chce přenést {{count}} {{descriptor}}\",\n        \"copied-text\": \"Text byl zkopírován do schránky\",\n        \"click-to-show\": \"Kliknutím zobrazíte\",\n        \"copied-text-error\": \"Zápis do schránky se nezdařil. Zkopírujte ručně!\",\n        \"offline\": \"Jste offline\",\n        \"online\": \"Jste zpět online\",\n        \"connected\": \"Připojeno\",\n        \"online-requirement-public-room\": \"Chcete-li vytvořit veřejnou místnost, musíte být online\",\n        \"online-requirement-pairing\": \"Chcete-li spárovat zařízení, musíte být online\",\n        \"connecting\": \"Připojování…\",\n        \"files-incorrect\": \"Soubory jsou nesprávné\",\n        \"file-transfer-completed\": \"Přenos souborů byl dokončen\",\n        \"message-transfer-completed\": \"Přenos zprávy byl dokončen\",\n        \"ios-memory-limit\": \"Odesílání souborů do iOS je možné pouze do velikosti 200 MB najednou\",\n        \"unfinished-transfers-warning\": \"Existují nedokončené přenosy Opravdu chcete zavřít PairDrop?\",\n        \"rate-limit-join-key\": \"Bylo dosaženo limitu. Počkejte 10 sekund a zkuste to znovu.\",\n        \"selected-peer-left\": \"Vybraný partner odešel\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Soubor byl přijat\",\n        \"file-received-plural\": \"Počet přijatých souborů: {{count}}\",\n        \"file-transfer-requested\": \"Požadován přenos souboru\",\n        \"message-received\": \"Zpráva přijata\",\n        \"image-transfer-requested\": \"Požadován přenos obrázku\",\n        \"message-received-plural\": \"Počet přijatých zpráv: {{count}}\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Kliknutím odešlete {{descriptor}}\",\n        \"click-to-send\": \"Kliknutím odešlete soubory nebo kliknutím pravým tlačítkem odešlete zprávu\",\n        \"transferring\": \"Přenáší se…\",\n        \"connection-hash\": \"Chcete-li ověřit bezpečnost šifrování typu end-to-end, porovnejte toto číslo zabezpečení na obou zařízeních\",\n        \"preparing\": \"Připravuje se…\",\n        \"waiting\": \"Čekání…\",\n        \"processing\": \"Zpracovává se…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/da.json",
    "content": "{\n    \"notifications\": {\n        \"public-room-left\": \"Forlod det offentlige rum {{publicRoomId}}\",\n        \"room-url-copied-to-clipboard\": \"Link til offentligt rum kopieret til udklipsholder\",\n        \"notifications-enabled\": \"Notifikationer aktiveret\",\n        \"notifications-permissions-error\": \"Notifikationstilladelsen er blevet blokeret, da brugeren har afvist tilladelsesprompten flere gange. Dette kan nulstilles i sideoplysninger, som du kan få adgang til ved at klikke på låseikonet ved siden af URL-linjen.\",\n        \"copied-text-error\": \"Skrivning til udklipsholder mislykkedes. Kopier manuelt!\",\n        \"ios-memory-limit\": \"Det er kun muligt at sende filer til iOS op til 200 MB på én gang\",\n        \"display-name-random-again\": \"Vist navn genereres tilfældigt igen\",\n        \"display-name-changed-permanently\": \"Det viste navn blec ændret permanent\",\n        \"display-name-changed-temporarily\": \"Vist navn blev kun ændret for denne session\",\n        \"download-successful\": \"{{descriptor}} hentet\",\n        \"pairing-tabs-error\": \"Det er umuligt at parre to webbrowserfaner\",\n        \"pairing-success\": \"Enheder parret\",\n        \"pairing-not-persistent\": \"Parrede enheder er ikke vedvarende\",\n        \"pairing-key-invalid\": \"Ugyldig nøgle\",\n        \"pairing-key-invalidated\": \"Nøglen {{key}} er ugyldig\",\n        \"pairing-cleared\": \"Alle enheder er frakoblet\",\n        \"public-room-id-invalid\": \"Ugyldigt rum-id\",\n        \"copied-to-clipboard\": \"Kopieret til udklipsholder\",\n        \"pair-url-copied-to-clipboard\": \"Link til at parre denne enhed kopieret til udklipsholder\",\n        \"copied-to-clipboard-error\": \"Kopiering ikke mulig. Kopier manuelt.\",\n        \"text-content-incorrect\": \"Tekstindholdet er forkert\",\n        \"file-content-incorrect\": \"Filens indhold er forkert\",\n        \"clipboard-content-incorrect\": \"Udklipsholderens indhold er forkert\",\n        \"link-received\": \"Link modtaget af {{name}} - Klik for at åbne\",\n        \"message-received\": \"Besked modtaget af {{name}} - Klik for at kopiere\",\n        \"click-to-download\": \"Klik for at hente\",\n        \"request-title\": \"{{name}} vil gerne overføre {{count}} {{descriptor}}\",\n        \"click-to-show\": \"Klik for at vise\",\n        \"copied-text\": \"Kopieret tekst til udklipsholder\",\n        \"offline\": \"Du er offline\",\n        \"online\": \"Du er online igen\",\n        \"connected\": \"Forbundet\",\n        \"online-requirement-pairing\": \"Du skal være online for at parre enheder\",\n        \"online-requirement-public-room\": \"Du skal være online for at oprette et offentligt rum\",\n        \"connecting\": \"Forbinder…\",\n        \"files-incorrect\": \"Filerne er forkerte\",\n        \"file-transfer-completed\": \"Filoverførsel gennemført\",\n        \"message-transfer-completed\": \"Beskedoverførsel gennemført\",\n        \"unfinished-transfers-warning\": \"Der er uafsluttede overførsler. Er du sikker på, at du vil lukke PairDrop?\",\n        \"rate-limit-join-key\": \"Satsgrænsen er nået. Vent 10 sekunder, og prøv igen.\",\n        \"selected-peer-left\": \"Valgt peer forlod\"\n    },\n    \"dialogs\": {\n        \"message_placeholder\": \"Tekst\",\n        \"base64-files\": \"filer\",\n        \"file-other-description-image\": \"og 1 andet billede\",\n        \"file-other-description-file\": \"og 1 anden fil\",\n        \"download-again\": \"Hent igen\",\n        \"system-language\": \"Systemsprog\",\n        \"pair-devices-qr-code_title\": \"Klik for at kopiere linket for at parre denne enhed\",\n        \"enter-key-from-another-device\": \"Indtast nøgle fra en anden enhed her.\",\n        \"temporary-public-room-title\": \"Midlertidigt offentligt rum\",\n        \"edit-paired-devices-title\": \"Rediger parrede enheder\",\n        \"auto-accept-instructions-2\": \"for automatisk at acceptere alle filer sendt fra den pågældende enhed.\",\n        \"pair-devices-title\": \"Par enheder permanent\",\n        \"input-key-on-this-device\": \"Indtast denne nøgle på en anden enhed\",\n        \"scan-qr-code\": \"eller scan QR-koden.\",\n        \"input-room-id-on-another-device\": \"Indtast dette rum-id på en anden enhed\",\n        \"enter-room-id-from-another-device\": \"Indtast rum-id fra en anden enhed for at deltage i rummet.\",\n        \"hr-or\": \"ELLER\",\n        \"pair\": \"Par\",\n        \"cancel\": \"Annuller\",\n        \"unpair\": \"Fjern parring\",\n        \"paired-device-removed\": \"Parret enhed er blevet fjernet.\",\n        \"paired-devices-wrapper_data-empty\": \"Ingen parrede enheder.\",\n        \"auto-accept-instructions-1\": \"Aktiver\",\n        \"auto-accept\": \"auto-accepter\",\n        \"close\": \"Luk\",\n        \"join\": \"Forbinde\",\n        \"leave\": \"Forlad\",\n        \"would-like-to-share\": \"gerne vil dele\",\n        \"accept\": \"Accepter\",\n        \"decline\": \"Nægt\",\n        \"has-sent\": \"har sendt:\",\n        \"share\": \"Del\",\n        \"download\": \"Hent\",\n        \"send-message-title\": \"Send besked\",\n        \"send-message-to\": \"Til:\",\n        \"message_title\": \"Indsæt besked for at sende\",\n        \"send\": \"Send\",\n        \"receive-text-title\": \"Besked modtaget\",\n        \"copy\": \"Kopier\",\n        \"base64-title-files\": \"Del filer\",\n        \"base64-title-text\": \"Del tekst\",\n        \"base64-processing\": \"Behandler…\",\n        \"base64-tap-to-paste\": \"Tryk her for at dele {{type}}\",\n        \"base64-paste-to-send\": \"Indsæt udklipsholder her for at dele {{type}}\",\n        \"base64-text\": \"tekst\",\n        \"file-other-description-image-plural\": \"og {{count}} andre billeder\",\n        \"file-other-description-file-plural\": \"og {{count}} andre filer\",\n        \"title-image\": \"Billede\",\n        \"title-file\": \"Fil\",\n        \"title-image-plural\": \"Billeder\",\n        \"title-file-plural\": \"Filer\",\n        \"receive-title\": \"{{descriptor}} Modtaget\",\n        \"language-selector-title\": \"Indstil sprog\",\n        \"public-room-qr-code_title\": \"Klik for at kopiere linket til det offentlige rum\",\n        \"approve\": \"godkend\",\n        \"share-text-title\": \"Del tekstbesked\",\n        \"share-text-subtitle\": \"Rediger besked, før du sender:\",\n        \"share-text-checkbox\": \"Vis altid denne dialogboks, når du deler tekst\",\n        \"close-toast_title\": \"Luk besked\"\n    },\n    \"about\": {\n        \"claim\": \"Den nemmeste måde at overføre filer på tværs af enheder\",\n        \"faq_title\": \"Ofte stillede spørgsmål\",\n        \"close-about_aria-label\": \"Luk Om PairDrop\",\n        \"github_title\": \"PairDrop på GitHub\",\n        \"buy-me-a-coffee_title\": \"Køb mig en kop kaffe!\",\n        \"tweet_title\": \"Tweet om PairDrop\",\n        \"mastodon_title\": \"Skriv om PairDrop på Mastodon\",\n        \"bluesky_title\": \"Følg os på BlueSky\",\n        \"custom_title\": \"Følg os\",\n        \"privacypolicy_title\": \"Åbn vores privatlivspolitik\"\n    },\n    \"header\": {\n        \"language-selector_title\": \"Indstil sprog\",\n        \"about_aria-label\": \"Åbn Om PairDrop\",\n        \"theme-auto_title\": \"Tilpas temaet til systemet automatisk\",\n        \"theme-light_title\": \"Brug altid lyst tema\",\n        \"theme-dark_title\": \"Brug altid mørkt tema\",\n        \"notification_title\": \"Aktiver notifikationer\",\n        \"install_title\": \"Installer PairDrop\",\n        \"pair-device_title\": \"Par dine enheder permanent\",\n        \"edit-paired-devices_title\": \"Rediger parrede enheder\",\n        \"join-public-room_title\": \"Deltag midlertidigt i det offentlige rum\",\n        \"cancel-share-mode\": \"Annuller\",\n        \"edit-share-mode\": \"Redigere\",\n        \"expand_title\": \"Udvid overskriftsknaprækken\",\n        \"about_title\": \"Om PairDrop\"\n    },\n    \"instructions\": {\n        \"no-peers-subtitle\": \"Par enheder, eller gå ind i et offentligt rum for at være synlig på andre netværk\",\n        \"x-instructions_desktop\": \"Klik for at sende filer eller højreklik for at sende en besked\",\n        \"activate-share-mode-base\": \"Åbn PairDrop på andre enheder for at sende\",\n        \"no-peers_data-drop-bg\": \"Slip for at vælge modtager\",\n        \"no-peers-title\": \"Åbn PairDrop på andre enheder for at sende filer\",\n        \"x-instructions_mobile\": \"Tryk for at sende filer, eller tryk længe for at sende en besked\",\n        \"x-instructions_data-drop-peer\": \"Slip for at sende til peer\",\n        \"x-instructions_data-drop-bg\": \"Slip for at vælge modtager\",\n        \"x-instructions-share-mode_desktop\": \"Klik for at sende {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"Tryk for at sende {{descriptor}}\",\n        \"activate-share-mode-and-other-file\": \"og 1 anden fil\",\n        \"activate-share-mode-and-other-files-plural\": \"og {{count}} andre filer\",\n        \"activate-share-mode-shared-text\": \"delt tekst\",\n        \"activate-share-mode-shared-file\": \"delt fil\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} delte filer\",\n        \"webrtc-requirement\": \"For at bruge denne PairDrop-instans skal WebRTC være aktiveret!\"\n    },\n    \"footer\": {\n        \"on-this-network_title\": \"Du kan blive opdaget af alle på dette netværk.\",\n        \"public-room-devices_title\": \"Du kan blive opdaget af enheder i dette offentlige rum uafhængigt af netværket.\",\n        \"known-as\": \"Du er kendt som:\",\n        \"display-name_data-placeholder\": \"Indlæser…\",\n        \"display-name_title\": \"Rediger dit enhedsnavn permanent\",\n        \"discovery\": \"Du kan blive opdaget:\",\n        \"on-this-network\": \"på dette netværk\",\n        \"paired-devices\": \"af parrede enheder\",\n        \"paired-devices_title\": \"Du kan til enhver tid blive opdaget af parrede enheder uafhængigt af netværket.\",\n        \"public-room-devices\": \"i rum {{roomId}}\",\n        \"traffic\": \"Trafikken er\",\n        \"routed\": \"dirigeret gennem serveren\",\n        \"webrtc\": \"hvis WebRTC ikke er tilgængelig.\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Fil modtaget\",\n        \"file-received-plural\": \"{{count}} Filer modtaget\",\n        \"file-transfer-requested\": \"Filoverførsel anmodet\",\n        \"image-transfer-requested\": \"Billedoverførsel anmodet\",\n        \"message-received\": \"Besked modtaget\",\n        \"message-received-plural\": \"{{count}} meddelelser modtaget\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Klik for at sende {{descriptor}}\",\n        \"click-to-send\": \"Klik for at sende filer eller højreklik for at sende en besked\",\n        \"connection-hash\": \"For at kontrollere sikkerheden for end-to-end-kryptering skal du sammenligne dette sikkerhedsnummer på begge enheder\",\n        \"preparing\": \"Forbereder…\",\n        \"waiting\": \"Venter…\",\n        \"processing\": \"Behandler…\",\n        \"transferring\": \"Overfører…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/de.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"Über PairDrop\",\n        \"notification_title\": \"Benachrichtigungen aktivieren\",\n        \"about_aria-label\": \"Über PairDrop öffnen\",\n        \"install_title\": \"PairDrop installieren\",\n        \"pair-device_title\": \"Kopple deine Geräte dauerhaft\",\n        \"edit-paired-devices_title\": \"Gekoppelte Geräte bearbeiten\",\n        \"theme-auto_title\": \"Systemstil verwenden\",\n        \"theme-dark_title\": \"Immer dunklen Stil verwenden\",\n        \"theme-light_title\": \"Immer hellen Stil verwenden\",\n        \"cancel-share-mode\": \"Fertig\",\n        \"language-selector_title\": \"Sprache Wählen\",\n        \"join-public-room_title\": \"Öffentlichen Raum temporär betreten\",\n        \"edit-share-mode\": \"Bearbeiten\",\n        \"expand_title\": \"Schaltflächenzeile ausklappen\"\n    },\n    \"dialogs\": {\n        \"share\": \"Teilen\",\n        \"download\": \"Download\",\n        \"pair-devices-title\": \"Geräte Dauerhaft Koppeln\",\n        \"input-key-on-this-device\": \"Gib diesen Schlüssel auf einem anderen Gerät ein\",\n        \"enter-key-from-another-device\": \"Gib den Schlüssel von einem anderen Gerät hier ein.\",\n        \"pair\": \"Koppeln\",\n        \"cancel\": \"Abbrechen\",\n        \"edit-paired-devices-title\": \"Gekoppelte Geräte Bearbeiten\",\n        \"paired-devices-wrapper_data-empty\": \"Keine gekoppelten Geräte.\",\n        \"close\": \"Schließen\",\n        \"accept\": \"Akzeptieren\",\n        \"decline\": \"Ablehnen\",\n        \"title-image\": \"Bild\",\n        \"title-file\": \"Datei\",\n        \"title-image-plural\": \"Bilder\",\n        \"title-file-plural\": \"Dateien\",\n        \"scan-qr-code\": \"oder scanne den QR-Code.\",\n        \"would-like-to-share\": \"möchte Folgendes teilen\",\n        \"send\": \"Senden\",\n        \"copy\": \"Kopieren\",\n        \"receive-text-title\": \"Nachricht Erhalten\",\n        \"file-other-description-image-plural\": \"und {{count}} andere Bilder\",\n        \"file-other-description-file-plural\": \"und {{count}} andere Dateien\",\n        \"auto-accept-instructions-1\": \"Aktiviere\",\n        \"auto-accept\": \"automatisch-akzeptieren\",\n        \"auto-accept-instructions-2\": \"um automatisch alle Dateien von diesem Gerät zu akzeptieren.\",\n        \"has-sent\": \"hat Folgendes gesendet:\",\n        \"send-message-title\": \"Nachricht Senden\",\n        \"send-message-to\": \"An:\",\n        \"base64-tap-to-paste\": \"Hier tippen, um {{type}} zu teilen\",\n        \"base64-paste-to-send\": \"Hier einfügen, um {{type}} zu teilen\",\n        \"base64-text\": \"Text\",\n        \"base64-files\": \"Dateien\",\n        \"base64-processing\": \"Bearbeitung läuft…\",\n        \"file-other-description-image\": \"und ein anderes Bild\",\n        \"file-other-description-file\": \"und eine andere Datei\",\n        \"receive-title\": \"{{descriptor}} Erhalten\",\n        \"download-again\": \"Erneuter Download\",\n        \"system-language\": \"Systemsprache\",\n        \"language-selector-title\": \"Sprache Einstellen\",\n        \"hr-or\": \"ODER\",\n        \"input-room-id-on-another-device\": \"Gib diese Raum-ID auf einem anderen Gerät ein\",\n        \"unpair\": \"Entkoppeln\",\n        \"leave\": \"Verlassen\",\n        \"join\": \"Betreten\",\n        \"enter-room-id-from-another-device\": \"Gib die Raum-ID von einem anderen Gerät hier ein.\",\n        \"temporary-public-room-title\": \"Temporärer Öffentlicher Raum\",\n        \"message_title\": \"Nachricht zum Senden hier einfügen\",\n        \"pair-devices-qr-code_title\": \"Klicke, um Link zum Koppeln mit diesem Gerät zu kopieren\",\n        \"public-room-qr-code_title\": \"Klicke, um Link zu diesem öffentlichen Raum zu kopieren\",\n        \"message_placeholder\": \"Text\",\n        \"close-toast_title\": \"Benachrichtigung schließen\",\n        \"share-text-checkbox\": \"Diesen Dialog immer anzeigen, wenn Text geteilt wird\",\n        \"base64-title-files\": \"Teile Dateien\",\n        \"approve\": \"bestätigen\",\n        \"paired-device-removed\": \"Gekoppeltes Gerät wurde entfernt.\",\n        \"share-text-title\": \"Teile Nachricht\",\n        \"share-text-subtitle\": \"Bearbeite Nachricht vor dem Senden:\",\n        \"base64-title-text\": \"Teile Text\"\n    },\n    \"about\": {\n        \"tweet_title\": \"Über PairDrop twittern\",\n        \"faq_title\": \"Häufig gestellte Fragen\",\n        \"close-about_aria-label\": \"Schließe Über PairDrop\",\n        \"github_title\": \"PairDrop auf GitHub\",\n        \"buy-me-a-coffee_title\": \"Kauf mir einen Kaffee!\",\n        \"claim\": \"Der einfachste Weg, Dateien zwischen Geräten zu übertragen\",\n        \"bluesky_title\": \"Folge uns auf BlueSky\",\n        \"privacypolicy_title\": \"Öffne unsere Datenschutzerklärung\",\n        \"mastodon_title\": \"Schreibe über PairDrop auf Mastodon\",\n        \"custom_title\": \"Folge uns\"\n    },\n    \"footer\": {\n        \"known-as\": \"Du wirst angezeigt als:\",\n        \"display-name_title\": \"Ändere deinen Gerätenamen dauerhaft\",\n        \"on-this-network\": \"in diesem Netzwerk\",\n        \"paired-devices\": \"für gekoppelte Geräte\",\n        \"traffic\": \"Datenverkehr wird\",\n        \"display-name_placeholder\": \"Lade…\",\n        \"routed\": \"durch den Server geleitet\",\n        \"webrtc\": \"wenn WebRTC nicht verfügbar ist.\",\n        \"display-name_data-placeholder\": \"Lade…\",\n        \"public-room-devices_title\": \"Du kannst von Geräten in diesem öffentlichen Raum gefunden werden, egal in welchem Netzwerk.\",\n        \"paired-devices_title\": \"Du kannst immer von gekoppelten Geräten gefunden werden, egal in welchem Netzwerk.\",\n        \"public-room-devices\": \"in Raum {{roomId}}\",\n        \"discovery\": \"Du bist sichtbar:\",\n        \"on-this-network_title\": \"Du kannst von jedem in diesem Netzwerk gefunden werden.\"\n    },\n    \"notifications\": {\n        \"link-received\": \"Link von {{name}} empfangen - Klicke um ihn zu öffnen\",\n        \"message-received\": \"Nachricht von {{name}} empfangen - Klicke um sie zu kopieren\",\n        \"click-to-download\": \"Klicken zum Download\",\n        \"copied-text\": \"Text in die Zwischenablage kopiert\",\n        \"connected\": \"Verbunden\",\n        \"pairing-success\": \"Geräte gekoppelt\",\n        \"display-name-random-again\": \"Anzeigename wird ab jetzt wieder zufällig generiert\",\n        \"pairing-tabs-error\": \"Es können keine zwei Webbrowser Tabs gekoppelt werden\",\n        \"pairing-not-persistent\": \"Gekoppelte Geräte sind nicht persistent\",\n        \"pairing-key-invalid\": \"Ungültiger Schlüssel\",\n        \"pairing-key-invalidated\": \"Schlüssel {{key}} wurde ungültig gemacht\",\n        \"copied-to-clipboard\": \"In die Zwischenablage kopiert\",\n        \"text-content-incorrect\": \"Textinhalt ist fehlerhaft\",\n        \"clipboard-content-incorrect\": \"Inhalt der Zwischenablage ist fehlerhaft\",\n        \"copied-text-error\": \"Konnte nicht in die Zwischenablage schreiben. Kopiere manuell!\",\n        \"file-content-incorrect\": \"Dateiinhalt ist fehlerhaft\",\n        \"notifications-enabled\": \"Benachrichtigungen aktiviert\",\n        \"offline\": \"Du bist offline\",\n        \"online\": \"Du bist wieder Online\",\n        \"unfinished-transfers-warning\": \"Es wurden noch nicht alle Übertragungen fertiggestellt. Möchtest du PairDrop wirklich schließen?\",\n        \"display-name-changed-permanently\": \"Anzeigename wurde dauerhaft geändert\",\n        \"download-successful\": \"{{descriptor}} heruntergeladen\",\n        \"pairing-cleared\": \"Alle Geräte entkoppelt\",\n        \"click-to-show\": \"Klicken zum Anzeigen\",\n        \"online-requirement\": \"Du musst online sein um Geräte zu koppeln.\",\n        \"display-name-changed-temporarily\": \"Anzeigename wurde nur für diese Session geändert\",\n        \"request-title\": \"{{name}} möchte {{count}}{{descriptor}} übertragen\",\n        \"connecting\": \"Verbindung wird hergestellt…\",\n        \"files-incorrect\": \"Dateien sind fehlerhaft\",\n        \"file-transfer-completed\": \"Dateitransfer abgeschlossen\",\n        \"message-transfer-completed\": \"Nachricht übertragen\",\n        \"rate-limit-join-key\": \"Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.\",\n        \"selected-peer-left\": \"Ausgewählter Peer ist gegangen\",\n        \"ios-memory-limit\": \"Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB\",\n        \"public-room-left\": \"Öffentlichen Raum {{publicRoomId}} verlassen\",\n        \"copied-to-clipboard-error\": \"Konnte nicht kopieren. Kopiere manuell.\",\n        \"public-room-id-invalid\": \"Ungültige Raum-ID\",\n        \"online-requirement-pairing\": \"Du musst online sein, um Geräte zu koppeln\",\n        \"online-requirement-public-room\": \"Du musst online sein, um öffentliche Räume erstellen zu können\",\n        \"notifications-permissions-error\": \"Benachrichtigungen wurden blockiert, weil der Nutzer die Berechtigungsanfrage mehrfach abgelehnt hat. Dies kann in den Einstellungen der Website zurückgesetzt werden, welche durch Klick auf das Schloss Symbol neben der URL Leiste erreicht werden können.\",\n        \"pair-url-copied-to-clipboard\": \"Link zum Koppeln mit diesem Gerät in Zwischenablage kopiert\",\n        \"room-url-copied-to-clipboard\": \"Link zu diesem öffentlichen Raum in Zwischenablage kopiert\"\n    },\n    \"instructions\": {\n        \"x-instructions_desktop\": \"Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Nachricht zu senden\",\n        \"no-peers-title\": \"Öffne PairDrop auf anderen Geräten, um Dateien zu senden\",\n        \"no-peers_data-drop-bg\": \"Hier ablegen, um Empfänger auszuwählen\",\n        \"no-peers-subtitle\": \"Kopple Geräte oder betritt einen öffentlichen Raum, um in anderen Netzwerken sichtbar zu sein\",\n        \"x-instructions-share-mode_desktop\": \"Klicke zum Senden {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"Tippe zum Senden {{descriptor}}\",\n        \"x-instructions_data-drop-peer\": \"Hier ablegen, um an Peer zu senden\",\n        \"x-instructions_data-drop-bg\": \"Loslassen um Empfänger auszuwählen\",\n        \"x-instructions_mobile\": \"Tippe, um Dateien zu senden oder tippe lange, um Nachrichten zu senden\",\n        \"activate-share-mode-base\": \"Öffne PairDrop auf anderen Geräten zum Senden\",\n        \"activate-share-mode-and-other-files-plural\": \"und {{count}} anderen Dateien\",\n        \"activate-share-mode-shared-text\": \"des geteilten Texts\",\n        \"webrtc-requirement\": \"Um diese PairDrop Instanz zu verwenden muss WebRTC aktiviert sein!\",\n        \"activate-share-mode-shared-files-plural\": \"der {{count}} geteilten Dateien\",\n        \"activate-share-mode-shared-file\": \"der geteilten Datei\",\n        \"activate-share-mode-and-other-file\": \"und 1 andere Datei\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"Dateitransfer beantragt\",\n        \"file-received\": \"Datei erhalten\",\n        \"file-received-plural\": \"{{count}} Dateien erhalten\",\n        \"message-received\": \"Nachricht erhalten\",\n        \"message-received-plural\": \"{{count}} Nachrichten erhalten\",\n        \"image-transfer-requested\": \"Transfer von Bildern beantragt\"\n    },\n    \"peer-ui\": {\n        \"click-to-send\": \"Klicke, um Dateien zu senden oder benutze einen Rechtsklick, um eine Textnachricht zu senden\",\n        \"connection-hash\": \"Um die Ende-zu-Ende Verschlüsselung zu verifizieren, vergleiche die Sicherheitsnummer auf beiden Geräten\",\n        \"waiting\": \"Warte…\",\n        \"click-to-send-share-mode\": \"Klicken um {{descriptor}} zu senden\",\n        \"transferring\": \"Übertragung läuft…\",\n        \"processing\": \"Bearbeitung läuft…\",\n        \"preparing\": \"Vorbereitung läuft…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/en.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"About PairDrop\",\n        \"language-selector_title\": \"Set Language\",\n        \"about_aria-label\": \"Open About PairDrop\",\n        \"theme-auto_title\": \"Adapt theme to system automatically\",\n        \"theme-light_title\": \"Always use light theme\",\n        \"theme-dark_title\": \"Always use dark theme\",\n        \"notification_title\": \"Enable notifications\",\n        \"install_title\": \"Install PairDrop\",\n        \"pair-device_title\": \"Pair your devices permanently\",\n        \"edit-paired-devices_title\": \"Edit paired devices\",\n        \"join-public-room_title\": \"Join public room temporarily\",\n        \"cancel-share-mode\": \"Cancel\",\n        \"edit-share-mode\": \"Edit\",\n        \"expand_title\": \"Expand header button row\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Release to select recipient\",\n        \"no-peers-title\": \"Open PairDrop on other devices to send files\",\n        \"no-peers-subtitle\": \"Pair devices or enter a public room to be discoverable on other networks\",\n        \"x-instructions_desktop\": \"Click to send files or right click to send a message\",\n        \"x-instructions_mobile\": \"Tap to send files or long tap to send a message\",\n        \"x-instructions_data-drop-peer\": \"Release to send to peer\",\n        \"x-instructions_data-drop-bg\": \"Release to select recipient\",\n        \"x-instructions-share-mode_desktop\": \"Click to send {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"Tap to send {{descriptor}}\",\n        \"activate-share-mode-base\": \"Open PairDrop on other devices to send\",\n        \"activate-share-mode-and-other-file\": \"and 1 other file\",\n        \"activate-share-mode-and-other-files-plural\": \"and {{count}} other files\",\n        \"activate-share-mode-shared-text\": \"shared text\",\n        \"activate-share-mode-shared-file\": \"shared file\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} shared files\",\n        \"webrtc-requirement\": \"To use this PairDrop instance, WebRTC must be enabled!\"\n    },\n    \"footer\": {\n        \"known-as\": \"You are known as:\",\n        \"display-name_data-placeholder\": \"Loading…\",\n        \"display-name_title\": \"Edit your device name permanently\",\n        \"discovery\": \"You can be discovered:\",\n        \"on-this-network\": \"on this network\",\n        \"on-this-network_title\": \"You can be discovered by everyone on this network.\",\n        \"paired-devices\": \"by paired devices\",\n        \"paired-devices_title\": \"You can be discovered by paired devices at all times independent of the network.\",\n        \"public-room-devices\": \"in room {{roomId}}\",\n        \"public-room-devices_title\": \"You can be discovered by devices in this public room independent of the network.\",\n        \"traffic\": \"Traffic is\",\n        \"routed\": \"routed through the server\",\n        \"webrtc\": \"if WebRTC is not available.\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"Pair Devices Permanently\",\n        \"input-key-on-this-device\": \"Input this key on another device\",\n        \"scan-qr-code\": \"or scan the QR-code.\",\n        \"enter-key-from-another-device\": \"Enter key from another device here.\",\n        \"temporary-public-room-title\": \"Temporary Public Room\",\n        \"input-room-id-on-another-device\": \"Input this room ID on another device\",\n        \"enter-room-id-from-another-device\": \"Enter room ID from another device to join room.\",\n        \"hr-or\": \"OR\",\n        \"pair\": \"Pair\",\n        \"cancel\": \"Cancel\",\n        \"edit-paired-devices-title\": \"Edit Paired Devices\",\n        \"unpair\": \"Unpair\",\n        \"paired-device-removed\": \"Paired device has been removed.\",\n        \"paired-devices-wrapper_data-empty\": \"No paired devices.\",\n        \"auto-accept-instructions-1\": \"Activate\",\n        \"auto-accept\": \"auto-accept\",\n        \"auto-accept-instructions-2\": \"to automatically accept all files sent from that device.\",\n        \"close\": \"Close\",\n        \"join\": \"Join\",\n        \"leave\": \"Leave\",\n        \"would-like-to-share\": \"would like to share\",\n        \"accept\": \"Accept\",\n        \"decline\": \"Decline\",\n        \"has-sent\": \"has sent:\",\n        \"share\": \"Share\",\n        \"download\": \"Download\",\n        \"send-message-title\": \"Send Message\",\n        \"send-message-to\": \"To:\",\n        \"message_title\": \"Insert message to send\",\n        \"message_placeholder\": \"Text\",\n        \"send\": \"Send\",\n        \"receive-text-title\": \"Message Received\",\n        \"copy\": \"Copy\",\n        \"base64-title-files\": \"Share Files\",\n        \"base64-title-text\": \"Share Text\",\n        \"base64-processing\": \"Processing…\",\n        \"base64-tap-to-paste\": \"Tap here to share {{type}}\",\n        \"base64-paste-to-send\": \"Paste clipboard here to share {{type}}\",\n        \"base64-text\": \"text\",\n        \"base64-files\": \"files\",\n        \"file-other-description-image\": \"and 1 other image\",\n        \"file-other-description-file\": \"and 1 other file\",\n        \"file-other-description-image-plural\": \"and {{count}} other images\",\n        \"file-other-description-file-plural\": \"and {{count}} other files\",\n        \"title-image\": \"Image\",\n        \"title-file\": \"File\",\n        \"title-image-plural\": \"Images\",\n        \"title-file-plural\": \"Files\",\n        \"receive-title\": \"{{descriptor}} Received\",\n        \"download-again\": \"Download again\",\n        \"language-selector-title\": \"Set Language\",\n        \"system-language\": \"System Language\",\n        \"public-room-qr-code_title\": \"Click to copy link to public room\",\n        \"pair-devices-qr-code_title\": \"Click to copy link to pair this device\",\n        \"approve\": \"approve\",\n        \"share-text-title\": \"Share Text Message\",\n        \"share-text-subtitle\": \"Edit message before sending:\",\n        \"share-text-checkbox\": \"Always show this dialog when sharing text\",\n        \"close-toast_title\": \"Close notification\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"Close About PairDrop\",\n        \"claim\": \"The easiest way to transfer files across devices\",\n        \"github_title\": \"PairDrop on GitHub\",\n        \"buy-me-a-coffee_title\": \"Buy me a coffee!\",\n        \"tweet_title\": \"Tweet about PairDrop\",\n        \"mastodon_title\": \"Write about PairDrop on Mastodon\",\n        \"bluesky_title\": \"Follow us on BlueSky\",\n        \"custom_title\": \"Follow us\",\n        \"privacypolicy_title\": \"Open our privacy policy\",\n        \"faq_title\": \"Frequently asked questions\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"Display name is changed permanently\",\n        \"display-name-changed-temporarily\": \"Display name is changed for this session only\",\n        \"display-name-random-again\": \"Display name is randomly generated again\",\n        \"download-successful\": \"{{descriptor}} downloaded\",\n        \"pairing-tabs-error\": \"Pairing two web browser tabs is impossible\",\n        \"pairing-success\": \"Devices paired\",\n        \"pairing-not-persistent\": \"Paired devices are not persistent\",\n        \"pairing-key-invalid\": \"Invalid key\",\n        \"pairing-key-invalidated\": \"Key {{key}} invalidated\",\n        \"pairing-cleared\": \"All devices unpaired\",\n        \"public-room-id-invalid\": \"Invalid room ID\",\n        \"public-room-left\": \"Left public room {{publicRoomId}}\",\n        \"copied-to-clipboard\": \"Copied to clipboard\",\n        \"pair-url-copied-to-clipboard\": \"Link to pair this device copied to clipboard\",\n        \"room-url-copied-to-clipboard\": \"Link to public room copied to clipboard\",\n        \"copied-to-clipboard-error\": \"Copying not possible. Copy manually.\",\n        \"text-content-incorrect\": \"Text content is incorrect\",\n        \"file-content-incorrect\": \"File content is incorrect\",\n        \"clipboard-content-incorrect\": \"Clipboard content is incorrect\",\n        \"notifications-enabled\": \"Notifications enabled\",\n        \"notifications-permissions-error\": \"Notifications permission has been blocked as the user has dismissed the permission prompt several times. This can be reset in Page Info which can be accessed by clicking the lock icon next to the URL bar.\",\n        \"link-received\": \"Link received by {{name}} - Click to open\",\n        \"message-received\": \"Message received by {{name}} - Click to copy\",\n        \"click-to-download\": \"Click to download\",\n        \"request-title\": \"{{name}} would like to transfer {{count}} {{descriptor}}\",\n        \"click-to-show\": \"Click to show\",\n        \"copied-text\": \"Copied text to clipboard\",\n        \"copied-text-error\": \"Writing to clipboard failed. Copy manually!\",\n        \"offline\": \"You are offline\",\n        \"online\": \"You are back online\",\n        \"connected\": \"Connected\",\n        \"online-requirement-pairing\": \"You need to be online to pair devices\",\n        \"online-requirement-public-room\": \"You need to be online to create a public room\",\n        \"connecting\": \"Connecting…\",\n        \"files-incorrect\": \"Files are incorrect\",\n        \"file-transfer-completed\": \"File transfer completed\",\n        \"ios-memory-limit\": \"Sending files to iOS is only possible up to 200 MB at once\",\n        \"message-transfer-completed\": \"Message transfer completed\",\n        \"unfinished-transfers-warning\": \"There are unfinished transfers. Are you sure you want to close PairDrop?\",\n        \"rate-limit-join-key\": \"Rate limit reached. Wait 10 seconds and try again.\",\n        \"selected-peer-left\": \"Selected peer left\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"File Received\",\n        \"file-received-plural\": \"{{count}} Files Received\",\n        \"file-transfer-requested\": \"File Transfer Requested\",\n        \"image-transfer-requested\": \"Image Transfer Requested\",\n        \"message-received\": \"Message Received\",\n        \"message-received-plural\": \"{{count}} Messages Received\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Click to send {{descriptor}}\",\n        \"click-to-send\": \"Click to send files or right click to send a message\",\n        \"connection-hash\": \"To verify the security of the end-to-end encryption, compare this security number on both devices\",\n        \"preparing\": \"Preparing…\",\n        \"waiting\": \"Waiting…\",\n        \"processing\": \"Processing…\",\n        \"transferring\": \"Transferring…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/es.json",
    "content": "{\n    \"header\": {\n        \"theme-auto_title\": \"Adaptar tema al sistema\",\n        \"language-selector_title\": \"Configurar Idioma\",\n        \"about_title\": \"Sobre PairDrop\",\n        \"about_aria-label\": \"Abrir Sobre PairDrop\",\n        \"cancel-share-mode\": \"Listo\",\n        \"install_title\": \"Instalar PairDrop\",\n        \"theme-dark_title\": \"Siempre usar tema oscuro\",\n        \"pair-device_title\": \"Empareja tus dispositivos permanentemente\",\n        \"join-public-room_title\": \"Unirse a una sala pública temporalmente\",\n        \"notification_title\": \"Activar notificaciones\",\n        \"edit-paired-devices_title\": \"Editar dispositivos emparejados\",\n        \"theme-light_title\": \"Siempre usar tema claro\",\n        \"expand_title\": \"Ampliar la fila de botones de la cabecera\",\n        \"edit-share-mode\": \"Editar\"\n    },\n    \"footer\": {\n        \"webrtc\": \"si WebRTC no está disponible.\",\n        \"public-room-devices_title\": \"Puedes ser descubierto por dispositivos en esta sala pública independientemente de la red.\",\n        \"display-name_data-placeholder\": \"Cargando…\",\n        \"display-name_title\": \"Edita el nombre de tu dispositivo de forma permanente\",\n        \"traffic\": \"El tráfico es\",\n        \"paired-devices_title\": \"Puedes ser descubierto por los dispositivos emparejados todo el tiempo independientemente de la red.\",\n        \"public-room-devices\": \"en la sala {{roomId}}\",\n        \"paired-devices\": \"por dispositivos emparejados\",\n        \"on-this-network\": \"en esta red\",\n        \"routed\": \"enrutado a través del servidor\",\n        \"discovery\": \"Puedes ser descubierto:\",\n        \"on-this-network_title\": \"Puedes ser descubierto por todos en esta red.\",\n        \"known-as\": \"Eres conocido como:\"\n    },\n    \"notifications\": {\n        \"request-title\": \"{{name}} quiere transferir {{count}} {{descriptor}}\",\n        \"unfinished-transfers-warning\": \"Hay transferencias no terminadas. ¿Estás seguro de que quieres cerrar PairDrop?\",\n        \"message-received\": \"Mensaje recibido por {{name}} - Haga clic para copiar\",\n        \"rate-limit-join-key\": \"Límite de intentos alcanzado. Espere 10 segundos y vuelva a intentarlo.\",\n        \"connecting\": \"Conectando…\",\n        \"pairing-key-invalidated\": \"Clave {{key}} invalidada\",\n        \"pairing-key-invalid\": \"Clave inválida\",\n        \"connected\": \"Connectado\",\n        \"pairing-not-persistent\": \"Los dispositivos emparejados no son persistentes\",\n        \"text-content-incorrect\": \"El contenido del texto es incorrecto\",\n        \"message-transfer-completed\": \"Transferencia del mensaje completada\",\n        \"file-transfer-completed\": \"Transferencia de archivos completada\",\n        \"file-content-incorrect\": \"El contenido del archivo es incorrecto\",\n        \"files-incorrect\": \"Los archivos son incorrectos\",\n        \"selected-peer-left\": \"Dispositivos seleccionados restantes\",\n        \"link-received\": \"Link recibido por {{name}} - Haga clic para abrir\",\n        \"online\": \"Estás de nuevo en línea\",\n        \"public-room-left\": \"Salió de la sala pública {{publicRoomId}}\",\n        \"copied-text\": \"Texto copiado al portapapeles\",\n        \"display-name-random-again\": \"El nombre mostrado se genera aleatoriamente nuevamente\",\n        \"display-name-changed-permanently\": \"El nombre para mostrar se ha cambiado permanentemente\",\n        \"copied-to-clipboard-error\": \"No es posible copiarlo. Cópielo manualmente.\",\n        \"pairing-success\": \"Dispositivos emparejados\",\n        \"clipboard-content-incorrect\": \"El contenido del portapapeles es incorrecto\",\n        \"display-name-changed-temporarily\": \"El nombre para mostrar se cambia sólo para esta sesión\",\n        \"copied-to-clipboard\": \"Copiado al portapapeles\",\n        \"offline\": \"Estás desconectado\",\n        \"pairing-tabs-error\": \"Emparejar dos pestañas del navegador es imposible\",\n        \"public-room-id-invalid\": \"ID de sala no válido\",\n        \"click-to-download\": \"Haga clic para descargar\",\n        \"pairing-cleared\": \"Todos los dispositivos han sido desemparejados\",\n        \"notifications-enabled\": \"Notificaciones habilitadas\",\n        \"online-requirement-pairing\": \"Debes estar en línea para emparejar dispositivos\",\n        \"ios-memory-limit\": \"Enviar archivos a iOS sólo admite hasta 200 MB a la vez\",\n        \"online-requirement-public-room\": \"Debes estar en línea para crear una sala pública\",\n        \"copied-text-error\": \"Error al escribir en el portapapeles. ¡Cópielo manualmente!\",\n        \"download-successful\": \"{{descriptor}} descargado\",\n        \"click-to-show\": \"Click para mostrar\",\n        \"notifications-permissions-error\": \"Las notificaciones se bloquearon porque el usuario rechazó la solicitud del permiso varias veces. Esto se puede restablecer en la configuración de la página web, a la que se quiere acceder haciendo clic en el icono del candado al lado de la barra con la URL.\",\n        \"pair-url-copied-to-clipboard\": \"El enlace para emparejar este dispositivo se copió en el portapapeles\",\n        \"room-url-copied-to-clipboard\": \"El enlace a la sala pública se copió en el portapapeles\"\n    },\n    \"instructions\": {\n        \"x-instructions_mobile\": \"Toque para enviar archivos o toque prologádamente para enviar un mensaje\",\n        \"x-instructions-share-mode_desktop\": \"Haga clic para enviar {{descriptor}}\",\n        \"activate-share-mode-and-other-files-plural\": \"y {{count}} archivos diferentes\",\n        \"x-instructions-share-mode_mobile\": \"Toque para enviar {{descriptor}}\",\n        \"activate-share-mode-base\": \"Abra PairDrop en otros dispositivos para enviar\",\n        \"no-peers-subtitle\": \"Empareje dispositivos o ingrese a una sala pública para que lo puedan encontrar en otras redes\",\n        \"activate-share-mode-shared-text\": \"texto compartido\",\n        \"x-instructions_desktop\": \"Haga clic para enviar archivos o haga clic derecho para enviar un mensaje\",\n        \"no-peers-title\": \"Abra PairDrop en otros dispositivos para enviar archivos\",\n        \"x-instructions_data-drop-peer\": \"Liberar para enviar a un par\",\n        \"x-instructions_data-drop-bg\": \"Liberar para seleccionar destinatario\",\n        \"no-peers_data-drop-bg\": \"Liberar para seleccionar destinatario\",\n        \"webrtc-requirement\": \"Para utilizar esta instancia de PairDrop, ¡WebRTC debe estar activado!\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} archivos compartidos\",\n        \"activate-share-mode-shared-file\": \"archivo compartido\",\n        \"activate-share-mode-and-other-file\": \"y 1 archivo más\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"Procesando…\",\n        \"click-to-send-share-mode\": \"Haga clic para enviar {{descriptor}}\",\n        \"click-to-send\": \"Haga clic para enviar archivos o haga clic derecho para enviar un mensaje\",\n        \"waiting\": \"Esperando…\",\n        \"connection-hash\": \"Para verificar la seguridad del cifrado de extremo a extremo, compare este número de seguridad en ambos dispositivos\",\n        \"preparing\": \"Preparando…\",\n        \"transferring\": \"Transferiendo…\"\n    },\n    \"dialogs\": {\n        \"base64-paste-to-send\": \"Pegar el portapapeles aquí para compartir {{type}}\",\n        \"auto-accept-instructions-2\": \"para aceptar automáticamente todos los archivos enviados desde ese dispositivo.\",\n        \"receive-text-title\": \"Mensaje Recibido\",\n        \"edit-paired-devices-title\": \"Editar Dispositivos Emparejados\",\n        \"cancel\": \"Cancelar\",\n        \"auto-accept-instructions-1\": \"Activar\",\n        \"pair-devices-title\": \"Emparejar dispositivos permanentemente\",\n        \"download\": \"Descargar\",\n        \"title-file\": \"Archivo\",\n        \"base64-processing\": \"Procesando…\",\n        \"decline\": \"Rechazar\",\n        \"receive-title\": \"{{descriptor}} Recibido\",\n        \"leave\": \"Salir\",\n        \"join\": \"Unirse\",\n        \"title-image-plural\": \"Imágenes\",\n        \"send\": \"Enviar\",\n        \"base64-tap-to-paste\": \"Pulse aquí para compartir {{type}}\",\n        \"base64-text\": \"texto\",\n        \"copy\": \"Copiar\",\n        \"file-other-description-image\": \"y una imagen mas\",\n        \"temporary-public-room-title\": \"Sala pública temporal\",\n        \"base64-files\": \"archivos\",\n        \"has-sent\": \"ha enviado:\",\n        \"file-other-description-file\": \"y otro archivo\",\n        \"close\": \"Cerrar\",\n        \"system-language\": \"Idioma del Sistema\",\n        \"unpair\": \"Desemparejar\",\n        \"title-image\": \"Imagen\",\n        \"file-other-description-file-plural\": \"y {{count}} archivos más\",\n        \"would-like-to-share\": \"quisiera compartir\",\n        \"send-message-to\": \"Para:\",\n        \"language-selector-title\": \"Configurar Idioma\",\n        \"pair\": \"Emparejar\",\n        \"hr-or\": \"O\",\n        \"scan-qr-code\": \"o escanea el código QR.\",\n        \"input-key-on-this-device\": \"Ingrese esta clave en otro dispositivo\",\n        \"download-again\": \"Descargar de nuevo\",\n        \"accept\": \"Aceptar\",\n        \"paired-devices-wrapper_data-empty\": \"Sin dispositivos emparejados.\",\n        \"enter-key-from-another-device\": \"Ingresa la clave de otro dispositivo aquí.\",\n        \"share\": \"Compartir\",\n        \"auto-accept\": \"aceptar automáticamente\",\n        \"title-file-plural\": \"Archivos\",\n        \"send-message-title\": \"Enviar Mensaje\",\n        \"input-room-id-on-another-device\": \"Ingrese el ID de esta sala en otro dispositivo\",\n        \"file-other-description-image-plural\": \"y {{count}} imágenes más\",\n        \"enter-room-id-from-another-device\": \"Ingresa el ID de la sala desde otro dispositivo para unirte a la sala.\",\n        \"message_title\": \"Insertar el mensaje a enviar\",\n        \"pair-devices-qr-code_title\": \"Haz clic para copiar el enlace para emparejar este dispositivo\",\n        \"public-room-qr-code_title\": \"Haz clic para copiar el enlace a la sala pública\",\n        \"message_placeholder\": \"Texto\",\n        \"close-toast_title\": \"Cerrar la notificación\",\n        \"share-text-checkbox\": \"Mostrar siempre este cuadro de diálogo al compartir texto\",\n        \"base64-title-files\": \"Compartir archivos\",\n        \"approve\": \"aprobar\",\n        \"paired-device-removed\": \"Se ha eliminado el dispositivo emparejado.\",\n        \"share-text-title\": \"Compartir un mensaje de texto\",\n        \"share-text-subtitle\": \"Edita el mensaje antes de enviarlo:\",\n        \"base64-title-text\": \"Compartir el texto\"\n    },\n    \"about\": {\n        \"claim\": \"La forma más sencilla de transferir archivos entre dispositivos\",\n        \"tweet_title\": \"Tweetea sobre PairDrop\",\n        \"close-about_aria-label\": \"Cerrar Sobre PairDrop\",\n        \"buy-me-a-coffee_title\": \"¡Cómprame un café!\",\n        \"github_title\": \"PairDrop en GitHub\",\n        \"faq_title\": \"Preguntas frecuentes\",\n        \"bluesky_title\": \"Síganos en BlueSky\",\n        \"privacypolicy_title\": \"Abrir nuestra política de privacidad\",\n        \"mastodon_title\": \"Escriba sobre PairDrop en Mastodon\",\n        \"custom_title\": \"Síguenos en\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"Transferencia de archivos solicitada\",\n        \"image-transfer-requested\": \"Transferencia de imagen solicitada\",\n        \"message-received-plural\": \"{{count}} Mensajes recibidos\",\n        \"message-received\": \"Mensaje recibido\",\n        \"file-received\": \"Archivo Recibido\",\n        \"file-received-plural\": \"{{count}} Archivos Recibidos\"\n    }\n}\n"
  },
  {
    "path": "public/lang/et.json",
    "content": "{\n    \"notifications\": {\n        \"rate-limit-join-key\": \"Jõudsid tegevuspiiranguni. Oota 10 sekundit ja proovi uuesti.\",\n        \"notifications-permissions-error\": \"Teavituste luba on keelatud, kuna oled mitu korda loataotluse sulgenud. Selle saad lähtestada lehe teabe menüüs, millele pääsed ligi aadressiribal oleva lukuikooni kaudu.\",\n        \"display-name-changed-permanently\": \"Kuvatav nimi on püsivalt muudetud\",\n        \"display-name-changed-temporarily\": \"Kuvatav nimi on muudetud vaid selle seansi jaoks\",\n        \"display-name-random-again\": \"Kuvatav nimi on taas juhuslikult genereeritud\",\n        \"pairing-not-persistent\": \"Paaritatud seadmed ei ole püsivad\",\n        \"public-room-id-invalid\": \"Sobimatu ruumi-ID\",\n        \"copied-to-clipboard\": \"Kopeeritud lõikelauale\",\n        \"copied-to-clipboard-error\": \"Kopeerimine pole võimalik. Kopeeri käsitsi.\",\n        \"room-url-copied-to-clipboard\": \"Avaliku ruumi link kopeeritud lõikelauale\",\n        \"clipboard-content-incorrect\": \"Lõikelaua sisu on sobimatu\",\n        \"link-received\": \"Link {{name}} poolt vastu võetud - klõpsa avamiseks\",\n        \"message-received\": \"Sõnum {{name}} poolt vastu võetud - klõpsa kopeerimiseks\",\n        \"connected\": \"Ühendatud\",\n        \"copied-text\": \"Tekst lõikelauale kopeeritud\",\n        \"copied-text-error\": \"Lõikelauale kirjutamine ebaõnnestus. Kopeeri käsitsi!\",\n        \"unfinished-transfers-warning\": \"Omad lõpetamata ülekandeid. Kas soovid kindlasti PairDropi sulgeda?\",\n        \"files-incorrect\": \"Failid on sobimatud\",\n        \"message-transfer-completed\": \"Sõnumiedastus lõpetatud\",\n        \"online-requirement-pairing\": \"Seadmete paaritamiseks pead võrgus olema\",\n        \"online-requirement-public-room\": \"Avaliku ruumi loomiseks pead võrgus olema\",\n        \"ios-memory-limit\": \"iOSil saab saata faile ainult 200 MB kaupa\",\n        \"selected-peer-left\": \"Valitud seade lahkus\",\n        \"pairing-cleared\": \"Kõik seadmepaaritused eemaldatud\",\n        \"offline\": \"Sa oled võrgust väljas\",\n        \"public-room-left\": \"Lahkusid avalikust ruumist {{publicRoomId}}\",\n        \"click-to-download\": \"Klõpsa allalaadimiseks\",\n        \"pairing-key-invalid\": \"Sobimatu võti\",\n        \"notifications-enabled\": \"Teavitused lubatud\",\n        \"text-content-incorrect\": \"Tekstisisu on sobimatu\",\n        \"file-content-incorrect\": \"Failisisu on sobimatu\",\n        \"click-to-show\": \"Klõpsa kuvamiseks\",\n        \"file-transfer-completed\": \"Failiedastus lõpetatud\",\n        \"request-title\": \"{{name}} soovib edastada {{count}} {{descriptor}}\",\n        \"online\": \"Sa oled tagasi võrgus\",\n        \"connecting\": \"Ühendamine…\",\n        \"download-successful\": \"{{descriptor}} allalaaditud\",\n        \"pairing-success\": \"Seadmed paaritatud\",\n        \"pairing-tabs-error\": \"Kahe brauserikaardi paaritamine on võimatu\",\n        \"pair-url-copied-to-clipboard\": \"Selle seadme paaritamise link kopeeritud lõikelauale\",\n        \"pairing-key-invalidated\": \"Võti {{key}} on muudetud kehtetuks\"\n    },\n    \"instructions\": {\n        \"x-instructions-share-mode_mobile\": \"Koputa, et saata {{descriptor}}\",\n        \"no-peers_data-drop-bg\": \"Vabasta saaja valimiseks\",\n        \"no-peers-title\": \"Ava failide saatmiseks PairDrop teistes seadmetes\",\n        \"no-peers-subtitle\": \"Paarita seadmed või sisesta avalik ruum, et olla teistes võrkudes avastatav\",\n        \"x-instructions_desktop\": \"Klõpsa failide saatmiseks või paremklõpsa sõnumi saatmiseks\",\n        \"x-instructions_mobile\": \"Koputa failide saatmiseks või hoia all sõnumi saatmiseks\",\n        \"x-instructions_data-drop-peer\": \"Vabasta sisu saatmiseks\",\n        \"activate-share-mode-base\": \"Saatmiseks ava PairDrop teistes seadmetes\",\n        \"activate-share-mode-and-other-file\": \"ja üks teine fail\",\n        \"activate-share-mode-shared-text\": \"jagatud tekst\",\n        \"webrtc-requirement\": \"Selle PairDrop eksemplari kasutamiseks peab WebRTC olema lubatud!\",\n        \"activate-share-mode-and-other-files-plural\": \"ja {{count}} teist faili\",\n        \"activate-share-mode-shared-file\": \"jagatud fail\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} jagatud faili\",\n        \"x-instructions-share-mode_desktop\": \"Klõpsa, et saata {{descriptor}}\",\n        \"x-instructions_data-drop-bg\": \"Vabasta seadme valimiseks\"\n    },\n    \"header\": {\n        \"theme-auto_title\": \"Kasuta automaatselt süsteemiteemat\",\n        \"theme-light_title\": \"Kasuta alati heledat teemat\",\n        \"cancel-share-mode\": \"Tühista\",\n        \"edit-share-mode\": \"Muuda\",\n        \"edit-paired-devices_title\": \"Muuda paaritatud seadmeid\",\n        \"join-public-room_title\": \"Liitu avaliku ruumiga ajutiselt\",\n        \"expand_title\": \"Laienda päise nupurida\",\n        \"about_title\": \"PairDropi teave\",\n        \"notification_title\": \"Luba teavitused\",\n        \"install_title\": \"Paigalda PairDrop\",\n        \"language-selector_title\": \"Määra keel\",\n        \"about_aria-label\": \"Ava PairDropi teave\",\n        \"pair-device_title\": \"Paarita oma seadmed püsivalt\",\n        \"theme-dark_title\": \"Kasuta alati tumedat teemat\"\n    },\n    \"footer\": {\n        \"known-as\": \"Sind tuntakse kui:\",\n        \"discovery\": \"Sind saab avastada:\",\n        \"on-this-network\": \"selles võrgus\",\n        \"paired-devices_title\": \"Paaritatud seadmed saavad sinu seadet igal ajal avastada, sõltumata võrgust.\",\n        \"on-this-network_title\": \"Igaüks selles võrgus saab sind avastada.\",\n        \"public-room-devices_title\": \"Selles avalikus ruumis olevad seadmed saavad sind avastada sõltumata võrgust.\",\n        \"webrtc\": \"kui WebRTC pole saadaval.\",\n        \"display-name_data-placeholder\": \"Laadimine…\",\n        \"display-name_title\": \"Muuda oma seadme nime püsivalt\",\n        \"paired-devices\": \"paaritatud seadmetes\",\n        \"public-room-devices\": \"ruumis {{roomId}}\",\n        \"routed\": \"suunatud läbi serveri\",\n        \"traffic\": \"Liiklus on\"\n    },\n    \"dialogs\": {\n        \"auto-accept-instructions-1\": \"Aktiveeri\",\n        \"pair-devices-title\": \"Paarita seadmed püsivalt\",\n        \"scan-qr-code\": \"või skanni QR-kood.\",\n        \"temporary-public-room-title\": \"Ajutine avalik ruum\",\n        \"input-room-id-on-another-device\": \"Sisesta see ruumi-ID teise seadmesse\",\n        \"paired-devices-wrapper_data-empty\": \"Paaritatud seadmed puuduvad.\",\n        \"auto-accept\": \"automaatne vastuvõtt\",\n        \"input-key-on-this-device\": \"Impordi see võti teises seadmes\",\n        \"enter-room-id-from-another-device\": \"Ruumiga liitumiseks sisesta teises seadmes see ruumi-ID.\",\n        \"paired-device-removed\": \"Paaritatud seade on eemaldatud.\",\n        \"would-like-to-share\": \"soovib jagada\",\n        \"accept\": \"Võta vastu\",\n        \"download\": \"Laadi alla\",\n        \"send-message-title\": \"Saada sõnum\",\n        \"send-message-to\": \"Seadmele:\",\n        \"auto-accept-instructions-2\": \"et automaatselt kõik sellest seadmest saadetud failid vastu võtta.\",\n        \"send\": \"Saada\",\n        \"language-selector-title\": \"Määra keel\",\n        \"receive-title\": \"{{descriptor}} vastuvõetud\",\n        \"approve\": \"võta vastu\",\n        \"close-toast_title\": \"Sulge teavitus\",\n        \"pair\": \"Paarita\",\n        \"close\": \"Sulge\",\n        \"hr-or\": \"VÕI\",\n        \"cancel\": \"Tühista\",\n        \"unpair\": \"Eemalda paardumine\",\n        \"join\": \"Liitu\",\n        \"leave\": \"Lahku\",\n        \"message_placeholder\": \"Tekst\",\n        \"base64-files\": \"failid\",\n        \"file-other-description-file\": \"ja üks teine fail\",\n        \"file-other-description-file-plural\": \"ja {{count}} teist faili\",\n        \"title-image\": \"Pilt\",\n        \"has-sent\": \"on saatnud:\",\n        \"share\": \"Jaga\",\n        \"message_title\": \"Sisesta saadetav sõnum\",\n        \"receive-text-title\": \"Sõnum vastuvõetud\",\n        \"copy\": \"Kopeeri\",\n        \"base64-title-text\": \"Jaga teksti\",\n        \"base64-title-files\": \"Jaga faile\",\n        \"pair-devices-qr-code_title\": \"Klõpsa, et kopeerida seadme paaritamise link\",\n        \"base64-tap-to-paste\": \"Koputa siia {{type}} jagamiseks\",\n        \"base64-paste-to-send\": \"Kleebi siia {{type}} jagamiseks\",\n        \"base64-text\": \"tekst\",\n        \"title-file-plural\": \"Failid\",\n        \"share-text-title\": \"Jaga tekstsõnumit\",\n        \"edit-paired-devices-title\": \"Muuda paaritatud seadmeid\",\n        \"enter-key-from-another-device\": \"Sisesta teise seadme võti siia.\",\n        \"system-language\": \"Süsteemikeel\",\n        \"share-text-subtitle\": \"Muuda sõnumit enne saatmist:\",\n        \"title-file\": \"Fail\",\n        \"title-image-plural\": \"Pildid\",\n        \"file-other-description-image\": \"ja üks teine pilt\",\n        \"download-again\": \"Laadi uuesti alla\",\n        \"file-other-description-image-plural\": \"ja {{count}} teist pilti\",\n        \"public-room-qr-code_title\": \"Klõpsa, et kopeerida avaliku ruumi link\",\n        \"share-text-checkbox\": \"Kuva teksti saatmisel alati see dialoog\",\n        \"base64-processing\": \"Töötlemine…\",\n        \"decline\": \"Keeldu\"\n    },\n    \"about\": {\n        \"claim\": \"Lihtsaim viis jagada faile üle seadmete\",\n        \"tweet_title\": \"Säutsu PairDropist\",\n        \"bluesky_title\": \"Jälgi meid BlueSkys\",\n        \"privacypolicy_title\": \"Ava meie privaatsuspoliitika\",\n        \"faq_title\": \"Korduma kippuvad küsimused\",\n        \"buy-me-a-coffee_title\": \"Osta mulle kohvi!\",\n        \"mastodon_title\": \"Kirjuta PairDropist Mastodonis\",\n        \"custom_title\": \"Jälgi meid\",\n        \"close-about_aria-label\": \"Sulge PairDropi teave\",\n        \"github_title\": \"PairDrop GitHubis\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"Failiedastust on taotletud\",\n        \"image-transfer-requested\": \"Pildiedastust on taotletud\",\n        \"file-received-plural\": \"{{count}} faili vastuvõetud\",\n        \"message-received-plural\": \"{{count}} sõnumit vastuvõetud\",\n        \"file-received\": \"Fail vastuvõetud\",\n        \"message-received\": \"Sõnum vastuvõetud\"\n    },\n    \"peer-ui\": {\n        \"connection-hash\": \"Otspunktkrüpteeringu turvalisuse kinnitamiseks võrdle seda arvu mõlemas seadmes\",\n        \"click-to-send\": \"Klõpsa failide saatmiseks või paremklõpsa sõnumi saatmiseks\",\n        \"preparing\": \"Valmistumine…\",\n        \"click-to-send-share-mode\": \"Klõpsa {{descriptor}} saatmiseks\",\n        \"waiting\": \"Ootamine…\",\n        \"processing\": \"Töötlemine…\",\n        \"transferring\": \"Edastamine…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/eu.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"PairDropi buruz\",\n        \"about_aria-label\": \"Ireki PairDropi buruz\",\n        \"cancel-share-mode\": \"Utzi\",\n        \"edit-share-mode\": \"Editatu\",\n        \"edit-paired-devices_title\": \"Editatu lotutako gailuak\",\n        \"theme-light_title\": \"Erabili beti gai argia\",\n        \"theme-dark_title\": \"Erabili beti gai iluna\",\n        \"notification_title\": \"Gaitu jakinarazpenak\",\n        \"install_title\": \"Instalatu PairDrop\",\n        \"pair-device_title\": \"Lotu zure gailuak betiko\",\n        \"expand_title\": \"Hedatu goiburuko botoien errenkada\",\n        \"language-selector_title\": \"Ezarri hizkuntza\",\n        \"theme-auto_title\": \"Aldatu gaia sistemak darabilenera\",\n        \"join-public-room_title\": \"Batu gela publikoa behin-behinean\"\n    },\n    \"instructions\": {\n        \"activate-share-mode-and-other-files-plural\": \"eta beste {{count}} fitxategi\",\n        \"no-peers-subtitle\": \"Lotu gailuak edo sartu gela publiko batean beste sareetan aurki zaitzaten\",\n        \"no-peers_data-drop-bg\": \"Jaregin hartzaileak hautatzeko\",\n        \"x-instructions_mobile\": \"Egin tap fitxategiak bidaltzeko edo luze sakatu mezua bidaltzeko\",\n        \"x-instructions_desktop\": \"Klikatu fitxategiak bidaltzeko edo klikatu eskumako botoiarekin mezu bat bidaltzeko\",\n        \"activate-share-mode-base\": \"Ireki PairDrop beste gailuetan bidaltzeko\",\n        \"activate-share-mode-shared-text\": \"partekatutako testua\",\n        \"x-instructions-share-mode_desktop\": \"Klikatu {{descriptor}} bidaltzeko\",\n        \"x-instructions-share-mode_mobile\": \"Egin tap {{descriptor}} bidaltzeko\",\n        \"activate-share-mode-shared-file\": \"partekatutako fitxategia\",\n        \"activate-share-mode-shared-files-plural\": \"partekatutako {{count}} fitxategi\",\n        \"webrtc-requirement\": \"PairDrop instantzia hau erabiltzeko, WebRTC gaitu behar da!\",\n        \"no-peers-title\": \"Ireki PairDrop beste gailuetan fitxategiak bidaltzeko\",\n        \"activate-share-mode-and-other-file\": \"eta beste fitxategi 1\",\n        \"x-instructions_data-drop-peer\": \"Jaregin kideari bidaltzeko\",\n        \"x-instructions_data-drop-bg\": \"Jaregin hartzailea hautatzeko\"\n    },\n    \"notifications\": {\n        \"online-requirement-pairing\": \"Linean egon behar zara gailuak lotzeko\",\n        \"copied-text-error\": \"Arbelean idazteak huts egin du. Egizu eskuz!\",\n        \"connecting\": \"Konektatzen…\",\n        \"display-name-changed-temporarily\": \"Pantaila-izena saio honetarako bakarrik aldatuko da\",\n        \"display-name-random-again\": \"Pantaila-izena ausaz sortuko da berriro\",\n        \"pairing-tabs-error\": \"Ezin dira nabigatzaileko bi fitxa lotu\",\n        \"pairing-success\": \"Gailuak lotu dira\",\n        \"pairing-not-persistent\": \"Gailuak ez dira behin-betiko lotu\",\n        \"pairing-key-invalid\": \"Gako okerra\",\n        \"public-room-id-invalid\": \"Gelaren IDa ez da baliozkoa\",\n        \"pair-url-copied-to-clipboard\": \"Gailu hau lotzeko esteka arbelera kopiatu da\",\n        \"room-url-copied-to-clipboard\": \"Gela publiko honetara sartzeko esteka arbelera kopiatu da\",\n        \"copied-to-clipboard-error\": \"Ezin da kopiatu. Egizu eskuz.\",\n        \"text-content-incorrect\": \"Testuaren edukia okerra da\",\n        \"file-content-incorrect\": \"Fitxategiaren edukia okerra da\",\n        \"clipboard-content-incorrect\": \"Arbeleko edukia okerra da\",\n        \"link-received\": \"{{name}}(e)k bidalitako esteka. Klikatu irekitzeko\",\n        \"notifications-permissions-error\": \"Jakinarazpenen baimena blokeatu egin da, erabiltzaileak behin baino gehiagotan ukatu baitu baimen-eskaera. Orriaren informazioan berrezar daiteke. Horretarako, URL barraren ondoko zerrapoaren ikonoan klik egin behar da.\",\n        \"message-received\": \"{{name}}(e)k bidalitako mezua. Klikatu kopiatzeko\",\n        \"click-to-download\": \"Klikatu deskargatzeko\",\n        \"request-title\": \"{{name}}(e)k {{count}} {{descriptor}} bidali nahi dizkizu\",\n        \"click-to-show\": \"Klikatu erakusteko\",\n        \"ios-memory-limit\": \"iOSek ez ditu 200 MB edo gehiagoko fitxategiak onartzen aldi berean\",\n        \"message-transfer-completed\": \"Mezuaren trukatzeak amaitu du\",\n        \"rate-limit-join-key\": \"Muga gainditu da. Itxaron 10 segundo eta saiatu berriro.\",\n        \"offline\": \"Lineaz kanpo zaude\",\n        \"files-incorrect\": \"Fitxategiak okerrak dira\",\n        \"display-name-changed-permanently\": \"Pantaila-izena betiko aldatuko da\",\n        \"connected\": \"Konektatuta\",\n        \"file-transfer-completed\": \"Fitxategien trukatzeak amaitu du\",\n        \"online-requirement-public-room\": \"Linean egon behar zara gela publiko bat sortzeko\",\n        \"notifications-enabled\": \"Jakinarazpenak gaitu dira\",\n        \"download-successful\": \"{{descriptor}} deskargatu da\",\n        \"pairing-cleared\": \"Gailu guztiak askatu dira\",\n        \"public-room-left\": \"{{publicRoomId}} gela publikotik irten zara\",\n        \"copied-to-clipboard\": \"Arbelera kopiatuta\",\n        \"copied-text\": \"Testua arbelera kopiatu da\",\n        \"online\": \"Berriro zaude linean\",\n        \"unfinished-transfers-warning\": \"Amaitu gabeko trukatzeak daude. Ziur PairDrop itxi nahi duzula?\",\n        \"selected-peer-left\": \"Hautatutako kideak alde egin du\",\n        \"pairing-key-invalidated\": \"{{key}} gakoa baliogabetu da\"\n    },\n    \"dialogs\": {\n        \"send\": \"Bidali\",\n        \"title-file\": \"Fitxategia\",\n        \"input-key-on-this-device\": \"Sartu gakoa beste gailu batean\",\n        \"enter-key-from-another-device\": \"Sartu beste gailu bateko gakoa hemen.\",\n        \"temporary-public-room-title\": \"Behin-behineko gela publikoa\",\n        \"input-room-id-on-another-device\": \"Sartu gela honen IDa beste gailu batean\",\n        \"enter-room-id-from-another-device\": \"Sartu beste gailu bateko gelaren IDa gelara sartzeko.\",\n        \"unpair\": \"Askatu\",\n        \"paired-device-removed\": \"Kendu egin da lotutako gailua.\",\n        \"paired-devices-wrapper_data-empty\": \"Ez dago lotutako gailurik.\",\n        \"auto-accept-instructions-1\": \"Aktibatu\",\n        \"auto-accept\": \"onartu automatikoki\",\n        \"close\": \"Itxi\",\n        \"join\": \"Sartu\",\n        \"would-like-to-share\": \"partekatu nahi du\",\n        \"decline\": \"Baztertu\",\n        \"has-sent\": \"bidali du:\",\n        \"receive-text-title\": \"Mezua jaso da\",\n        \"copy\": \"Kopiatu\",\n        \"base64-title-files\": \"Partekatu fitxategiak\",\n        \"base64-title-text\": \"Partekatu testua\",\n        \"base64-tap-to-paste\": \"Egin tap hemen {{type}} partekatzeko\",\n        \"base64-processing\": \"Prozesatzen…\",\n        \"base64-paste-to-send\": \"Itsatsi arbelekoa hemen {{type}} partekatzeko\",\n        \"base64-text\": \"testua\",\n        \"base64-files\": \"fitxategiak\",\n        \"file-other-description-image\": \"eta beste irudi 1\",\n        \"file-other-description-file\": \"eta beste fitxategi 1\",\n        \"file-other-description-image-plural\": \"eta beste {{count}} irudi\",\n        \"title-image\": \"Irudia\",\n        \"receive-title\": \"{{descriptor}} jaso da\",\n        \"download-again\": \"Deskargatu berriro\",\n        \"system-language\": \"Sistemak darabilena\",\n        \"public-room-qr-code_title\": \"Klikatu gela publikoaren esteka kopiatzeko\",\n        \"approve\": \"onartu\",\n        \"title-file-plural\": \"Fitxategiak\",\n        \"share-text-checkbox\": \"Erakutsi beti leiho hau testua partekatzerakoan\",\n        \"pair-devices-title\": \"Lotu gailuak betiko\",\n        \"scan-qr-code\": \"edo eskaneatu QR kodea.\",\n        \"hr-or\": \"EDO\",\n        \"pair\": \"Lotu\",\n        \"cancel\": \"Utzi\",\n        \"edit-paired-devices-title\": \"Editatu lotutako gailuak\",\n        \"accept\": \"Onartu\",\n        \"share\": \"Partekatu\",\n        \"download\": \"Deskargatu\",\n        \"send-message-title\": \"Bidali mezua\",\n        \"send-message-to\": \"Honi:\",\n        \"message_placeholder\": \"Testua\",\n        \"message_title\": \"Sartu bidaltzeko mezua\",\n        \"title-image-plural\": \"Irudiak\",\n        \"pair-devices-qr-code_title\": \"Klikatu gailu hau lotzeko esteka kopiatzeko\",\n        \"leave\": \"Irten\",\n        \"share-text-title\": \"Partekatu testuzko mezua\",\n        \"auto-accept-instructions-2\": \"automatikoki onartzeko gailu horretatik bidalitako fitxategi guztiak.\",\n        \"file-other-description-file-plural\": \"eta beste {{count}} fitxategi\",\n        \"language-selector-title\": \"Ezarri hizkuntza\",\n        \"share-text-subtitle\": \"Editatu mezua bidali baino lehen:\",\n        \"close-toast_title\": \"Itxi jakinarazpena\"\n    },\n    \"about\": {\n        \"custom_title\": \"Jarraitu iezaguzu\",\n        \"claim\": \"Gailuen artean fitxategiak trukatzeko modurik errazena\",\n        \"github_title\": \"PairDrop GitHuben\",\n        \"buy-me-a-coffee_title\": \"Erosidazu kafe bat!\",\n        \"privacypolicy_title\": \"Ireki gure pribatutasun politika\",\n        \"tweet_title\": \"Egin txio PairDropi buruz\",\n        \"faq_title\": \"Maiz Egindako Galderak\",\n        \"mastodon_title\": \"Idatzi Mastodonen PairDropi buruz\",\n        \"bluesky_title\": \"Jarrai iezaguzu BlueSkyn\",\n        \"close-about_aria-label\": \"Itxi PairDropi buruz\"\n    },\n    \"footer\": {\n        \"known-as\": \"Zure izena honakoa da:\",\n        \"display-name_data-placeholder\": \"Kargatzen…\",\n        \"display-name_title\": \"Editatu zure gailuaren izena betiko\",\n        \"on-this-network_title\": \"Sare honetako edonork aurki zaitzake.\",\n        \"paired-devices\": \"lotutako gailuek\",\n        \"public-room-devices_title\": \"Gela publiko honetan dauden gailuek aurki zaitzakete, zauden sarean zaudela.\",\n        \"traffic\": \"Trafikoa:\",\n        \"routed\": \"zerbitzaritik bideratuta\",\n        \"discovery\": \"Aurki zaitzakete:\",\n        \"webrtc\": \"WebRTC erabilgarri ez badago.\",\n        \"on-this-network\": \"sare honetan\",\n        \"public-room-devices\": \"{{roomId}} gelan\",\n        \"paired-devices_title\": \"Lotutako gailuek uneoro aurki zaitzakete, zauden sarearen zaudela.\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Fitxategia jaso da\",\n        \"file-received-plural\": \"{{count}} fitxategi jaso dira\",\n        \"image-transfer-requested\": \"Irudiaren trukaketa eskatu da\",\n        \"message-received\": \"Mezua jaso da\",\n        \"message-received-plural\": \"{{count}} mezu jaso dira\",\n        \"file-transfer-requested\": \"Fitxategiaren trukaketa eskatu da\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Klikatu {{descriptor}} bidaltzeko\",\n        \"preparing\": \"Prestatzen…\",\n        \"waiting\": \"Zain…\",\n        \"processing\": \"Prozesatzen…\",\n        \"transferring\": \"Trukatzen…\",\n        \"connection-hash\": \"Zifratzearen segurtasuna muturretik muturrera egiaztatzeko, konparatu segurtasun-zenbaki hau bi gailuetan\",\n        \"click-to-send\": \"Klikatu fitxategiak bidaltzeko edo klikatu eskumako botoiarekin mezu bat bidaltzeko\"\n    }\n}\n"
  },
  {
    "path": "public/lang/fa.json",
    "content": "{\n    \"header\": {\n        \"theme-light_title\": \"همیشه از پوسته روشن استفاده شود\",\n        \"theme-dark_title\": \"همیشه از پوسته تیره استفاده شود\",\n        \"install_title\": \"نصب پیردراپ\",\n        \"cancel-share-mode\": \"لغو\",\n        \"edit-share-mode\": \"ویرایش\",\n        \"expand_title\": \"گسترش ردیف دکمه سرایند\",\n        \"about_title\": \"درباره پیردراپ\",\n        \"language-selector_title\": \"تنظیم زبان\",\n        \"theme-auto_title\": \"همسان‌سازی خودکار پوسته با سامانه\",\n        \"notification_title\": \"فعال‌سازی آگاهی‌ها\",\n        \"pair-device_title\": \"جفت‌کردن دائمی دستگاه‌های‌تان\",\n        \"join-public-room_title\": \"پیوستن موقتی به اتاق عمومی\",\n        \"edit-paired-devices_title\": \"ویرایش دستگاه‌های جفت‌شده\",\n        \"about_aria-label\": \"باز کردن درباره پیردراپ\"\n    },\n    \"instructions\": {\n        \"no-peers-title\": \"برای فرستادن پرونده‌ها، پیردراپ را روی دستگاه‌های دیگر باز کنید\",\n        \"no-peers-subtitle\": \"دستگاه‌ها را جفت کنید و یا با پیوستن به اتاق عمومی، روی دیگر شبکه‌ها قابل شناسایی شوید\",\n        \"x-instructions_mobile\": \"برای فرستادن پرونده‌ها کلیک کنید یا با لمس طولانی، پیامی بفرستید\",\n        \"x-instructions_data-drop-bg\": \"با رهاکردن، دریافت‌کننده را انتخاب کنید\",\n        \"x-instructions-share-mode_desktop\": \"برای فرستادن {{descriptor}} کلیک کنید\",\n        \"x-instructions-share-mode_mobile\": \"برای فرستادن {{descriptor}} ضربه بزنید\",\n        \"activate-share-mode-base\": \"برای فرستادن، پیردراپ را روی دستگاه‌های دیگر باز کنید\",\n        \"activate-share-mode-and-other-file\": \"و یک پرونده دیگر\",\n        \"activate-share-mode-and-other-files-plural\": \"و {{count}} پرونده دیگر\",\n        \"activate-share-mode-shared-file\": \"پرونده هم‌رسانی شده\",\n        \"activate-share-mode-shared-text\": \"متن هم‌رسانی شده\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} پرونده هم‌رسانی شده\",\n        \"webrtc-requirement\": \"برای استفاده از این نمونه پیردراپ بایستی WebRTC فعال باشد!\",\n        \"x-instructions_data-drop-peer\": \"با رهاکردن، پرونده را بفرستید\",\n        \"x-instructions_desktop\": \"برای فرستادن پرونده‌ها کلیک کرده و یا با کلیک راست، پیامی بفرستید\",\n        \"no-peers_data-drop-bg\": \"با رهاکردن، دریافت‌کننده را انتخاب کنید\"\n    },\n    \"footer\": {\n        \"known-as\": \"شما به این عنوان شناخته می‌شوید:\",\n        \"display-name_data-placeholder\": \"در حال بار شدن…\",\n        \"display-name_title\": \"ویرایش دائمی نام دستگاه شما\",\n        \"discovery\": \"قابل شناسایی هستید:\",\n        \"on-this-network\": \"روی این شبکه\",\n        \"paired-devices\": \"توسط دستگاه‌های جفت‌شده\",\n        \"on-this-network_title\": \"شما توسط همه افراد این شبکه قابل شناسایی هستید.\",\n        \"paired-devices_title\": \"شما در هر زمان فارق از شبکه، توسط دستگاه‌های جفت‌شده قابل شناسایی هستید.\",\n        \"public-room-devices\": \"در اتاق {{roomId}}\",\n        \"public-room-devices_title\": \"شما در این اتاق عمومی فارق از شبکه، توسط دستگاه‌ها قابل شناسایی هستید.\",\n        \"traffic\": \"ترافیک\",\n        \"routed\": \"از طریق کارساز(سرور) مسیریابی می‌شود\",\n        \"webrtc\": \"اگر WebRTC در دسترس نیست.\"\n    },\n    \"dialogs\": {\n        \"accept\": \"پذیرفتن\",\n        \"message_placeholder\": \"متن\",\n        \"send\": \"ارسال\",\n        \"base64-files\": \"پرونده‌ها\",\n        \"file-other-description-image\": \"و 1 تصویر دیگر\",\n        \"language-selector-title\": \"تنظیم زبان\",\n        \"system-language\": \"زبان سیستم\",\n        \"public-room-qr-code_title\": \"برای رونوشت پیوند به اتاق عمومی کلیک کنید\",\n        \"pair-devices-qr-code_title\": \"برای رونوشت پیوند به جفت‌کردن این دستگاه کلیک کنید\",\n        \"approve\": \"اثبات\",\n        \"share-text-title\": \"هم‌رسانی پیام متنی\",\n        \"share-text-subtitle\": \"قبل از ارسال پیام را ویرایش کنید:\",\n        \"share-text-checkbox\": \"همیشه این پیام را هنگام هم‌رسانی متن نشان دهید\",\n        \"close-toast_title\": \"بستن اعلان\",\n        \"pair-devices-title\": \"جفت‌کردن دستگاه‌ها به‌طور دائمی\",\n        \"input-key-on-this-device\": \"این کلید را روی دستگاه دیگر وارد کنید\",\n        \"scan-qr-code\": \"یا رمزینه QR را بررسی کنید.\",\n        \"enter-key-from-another-device\": \"کلید را از دستگاه دیگر اینجا وارد کنید.\",\n        \"temporary-public-room-title\": \"اتاق عمومی موقتی\",\n        \"input-room-id-on-another-device\": \"این شناسه اتاق را روی دستگاه دیگر وارد کنید\",\n        \"enter-room-id-from-another-device\": \"شناسه اتاق را از دستگاه دیگر وارد کنید تا به اتاق بپیوندید.\",\n        \"hr-or\": \"یا\",\n        \"pair\": \"جفت‌کردن\",\n        \"cancel\": \"لغو\",\n        \"unpair\": \"جدا کردن\",\n        \"paired-device-removed\": \"دستگاه جفت‌شده حذف شد.\",\n        \"paired-devices-wrapper_data-empty\": \"هیچ دستگاه جفت‌شده‌ای وجود ندارد.\",\n        \"auto-accept-instructions-1\": \"فعال‌سازی\",\n        \"auto-accept\": \"پذیرش خودکار\",\n        \"auto-accept-instructions-2\": \"تا به‌طور خودکار تمام پرونده‌های ارسال شده از آن دستگاه را بپذیرید.\",\n        \"close\": \"بستن\",\n        \"join\": \"پیوستن\",\n        \"leave\": \"ترک\",\n        \"would-like-to-share\": \"می خواهم به اشتراک بگذارم\",\n        \"edit-paired-devices-title\": \"ویرایش دستگاه‌های جفت‌شده\",\n        \"decline\": \"رد کردن\",\n        \"has-sent\": \"ارسال کرده است:\",\n        \"share\": \"هم‌رسانی\",\n        \"download\": \"دریافت\",\n        \"send-message-title\": \"ارسال پیام\",\n        \"send-message-to\": \"به:\",\n        \"message_title\": \"متن پیام را وارد کنید\",\n        \"receive-text-title\": \"پیام دریافت شد\",\n        \"copy\": \"رونوشت\",\n        \"base64-title-files\": \"هم‌رسانی پرونده‌ها\",\n        \"base64-title-text\": \"هم‌رسانی متن\",\n        \"base64-processing\": \"در حال پردازش…\",\n        \"base64-tap-to-paste\": \"برای هم‌رسانی {{type}} اینجا ضربه بزنید\",\n        \"base64-paste-to-send\": \"برای هم‌رسانی {{type}} اینجا کلیک راست کنید\",\n        \"base64-text\": \"متن\",\n        \"file-other-description-file\": \"و 1 پرونده دیگر\",\n        \"file-other-description-image-plural\": \"و {{count}} تصویر دیگر\",\n        \"file-other-description-file-plural\": \"و {{count}} پرونده دیگر\",\n        \"title-image\": \"تصویر\",\n        \"title-file\": \"پرونده\",\n        \"title-image-plural\": \"تصاویر\",\n        \"title-file-plural\": \"پرونده‌ها\",\n        \"receive-title\": \"{{descriptor}} دریافت شد\",\n        \"download-again\": \"دوباره دریافت کنید\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"بستن درباره پیردراپ\",\n        \"claim\": \"ساده‌ترین راه برای انتقال پرونده‌ها بین دستگاه‌ها\",\n        \"github_title\": \"پیردراپ در گیت‌هاب\",\n        \"buy-me-a-coffee_title\": \"برای من قهوه بخرید!\",\n        \"tweet_title\": \"در مورد پیردراپ توییت کنید\",\n        \"mastodon_title\": \"در مورد پیردراپ در ماستودون بنویسید\",\n        \"bluesky_title\": \"ما را در BlueSky دنبال کنید\",\n        \"custom_title\": \"ما را دنبال کنید\",\n        \"privacypolicy_title\": \"سیاست حفظ حریم خصوصی ما را باز کنید\",\n        \"faq_title\": \"سوالات متداول\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"نام نمایشی برای همیشه تغییر کرد\",\n        \"display-name-changed-temporarily\": \"نام نمایشی فقط برای این نشست تغییر کرد\",\n        \"display-name-random-again\": \"نام نمایشی دوباره به‌طور تصادفی تولید شد\",\n        \"download-successful\": \"{{descriptor}} دریافت شد\",\n        \"pairing-tabs-error\": \"جفت‌کردن دو تب مرورگر وب ممکن نیست\",\n        \"pairing-success\": \"دستگاه‌ها جفت شدند\",\n        \"pairing-not-persistent\": \"دستگاه‌های جفت‌شده پایدار نیستند\",\n        \"pairing-key-invalid\": \"کلید نامعتبر است\",\n        \"pairing-key-invalidated\": \"کلید {{key}} نامعتبر شد\",\n        \"pairing-cleared\": \"تمام دستگاه‌ها جدا شدند\",\n        \"public-room-id-invalid\": \"شناسه اتاق نامعتبر است\",\n        \"public-room-left\": \"اتاق عمومی {{publicRoomId}} را ترک کردید\",\n        \"copied-to-clipboard\": \"به بُریده‏دان رونوشت شد\",\n        \"pair-url-copied-to-clipboard\": \"پیوند جفت‌کردن این دستگاه به بُریده‏دان رونوشت شد\",\n        \"room-url-copied-to-clipboard\": \"پیوند اتاق عمومی به بُریده‏دان رونوشت شد\",\n        \"copied-to-clipboard-error\": \"رونوشت کردن ممکن نیست. به‌صورت دستی رونوشت کنید.\",\n        \"text-content-incorrect\": \"محتوای متن نادرست است\",\n        \"file-content-incorrect\": \"محتوای پرونده نادرست است\",\n        \"online\": \"شما دوباره برخط هستید\",\n        \"connected\": \"متصل شد\",\n        \"online-requirement-pairing\": \"شما باید برخط باشید تا دستگاه‌ها را جفت کنید\",\n        \"online-requirement-public-room\": \"شما باید برخط باشید تا اتاق عمومی ایجاد کنید\",\n        \"unfinished-transfers-warning\": \"انتقال‌های ناتمام وجود دارد. آیا مطمئن هستید که می‌خواهید پیردراپ را ببندید؟\",\n        \"rate-limit-join-key\": \"محدودیت نرخ رسید. 10 ثانیه صبر کنید و دوباره تلاش کنید.\",\n        \"selected-peer-left\": \"همتای انتخاب شده ترک کرد\",\n        \"notifications-permissions-error\": \"مجوز آگاهی‌ها به دلیل اینکه کاربر چندین بار پنجره مجوز را رد کرده است، مسدود شده. این می‌تواند در اطلاعات صفحه که با کلیک بر روی نماد قفل در کنار نوار URL قابل دسترسی است، بازنشانی شود.\",\n        \"clipboard-content-incorrect\": \"محتوای بُریده‏دان نادرست است\",\n        \"link-received\": \"پیوند از {{name}} دریافت شد - برای باز کردن کلیک کنید\",\n        \"notifications-enabled\": \"آگاهی‌ها فعال شدند\",\n        \"click-to-show\": \"برای نمایش کلیک کنید\",\n        \"message-received\": \"پیام از {{name}} دریافت شد - برای رونوشت کردن کلیک کنید\",\n        \"click-to-download\": \"برای دریافت کلیک کنید\",\n        \"request-title\": \"{{name}} می‌خواهد {{count}} {{descriptor}} را منتقل کند\",\n        \"copied-text\": \"متن به بُریده‏دان رونوشت شد\",\n        \"copied-text-error\": \"نوشتن به بُریده‏دان ناموفق بود. به‌صورت دستی رونوشت کنید!\",\n        \"offline\": \"شما برون‌خط هستید\",\n        \"connecting\": \"در حال اتصال…\",\n        \"files-incorrect\": \"پرونده‌ها نادرست هستند\",\n        \"file-transfer-completed\": \"انتقال پرونده کامل شد\",\n        \"ios-memory-limit\": \"ارسال پرونده‌ها به iOS فقط تا 200 مگابایت در یک بار ممکن است\",\n        \"message-transfer-completed\": \"انتقال پیام کامل شد\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"پرونده دریافت شد\",\n        \"file-received-plural\": \"{{count}} پرونده دریافت شد\",\n        \"file-transfer-requested\": \"درخواست انتقال پرونده\",\n        \"image-transfer-requested\": \"درخواست انتقال تصویر\",\n        \"message-received\": \"پیام دریافت شد\",\n        \"message-received-plural\": \"{{count}} پیام دریافت شد\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"برای ارسال {{descriptor}} کلیک کنید\",\n        \"click-to-send\": \"برای ارسال پرونده‌ها کلیک کنید یا با کلیک راست، پیامی بفرستید\",\n        \"connection-hash\": \"برای اثبات امنیت رمزگذاری انتها به انتها، این شماره امنیتی را در هر دو دستگاه مقایسه کنید\",\n        \"preparing\": \"در حال آماده‌سازی…\",\n        \"waiting\": \"در حال انتظار…\",\n        \"processing\": \"در حال پردازش…\",\n        \"transferring\": \"در حال انتقال…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/fi.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"Tietoja PairDropista\",\n        \"language-selector_title\": \"Valitse kieli\",\n        \"about_aria-label\": \"Avaa tietoja PairDropista\",\n        \"theme-auto_title\": \"Käytä samaa teemaa kuin järjestelmä\",\n        \"theme-light_title\": \"Käytä aina vaaleaa teemaa\",\n        \"theme-dark_title\": \"Käytä aina tummaa teemaa\",\n        \"notification_title\": \"Laita ilmoitukset päälle\",\n        \"install_title\": \"Asenna PairDrop\",\n        \"pair-device_title\": \"Yhdistä laitteesi pysyvästi\",\n        \"edit-paired-devices_title\": \"Muokkaa yhdistettyjä laitteita\",\n        \"join-public-room_title\": \"Liity julkiseen huoneeseen väliaikaisesti\",\n        \"cancel-share-mode\": \"Peruuta\",\n        \"edit-share-mode\": \"Muokkaa\"\n    },\n    \"instructions\": {\n        \"no-peers-title\": \"Avaa PairDrop muilla laitteilla lähettääksesi tiedostoja\",\n        \"no-peers-subtitle\": \"Yhdistä laite tai liity julkiseen huoneeseen, jotta olet löydettävissä muissa verkoissa\",\n        \"x-instructions_desktop\": \"Paina lähettääksesi tiedoston tai klikkaa oikealla painikkeella lähettääksesi viestin\"\n    }\n}\n"
  },
  {
    "path": "public/lang/fr.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"À propos de PairDrop\",\n        \"language-selector_title\": \"Choix de la langue\",\n        \"about_aria-label\": \"Ouvrir à propos de PairDrop\",\n        \"theme-auto_title\": \"Adapter le thème au système\",\n        \"theme-light_title\": \"Toujours utiliser le thème clair\",\n        \"theme-dark_title\": \"Toujours utiliser le thème sombre\",\n        \"notification_title\": \"Activer les notifications\",\n        \"install_title\": \"Installer PairDrop\",\n        \"pair-device_title\": \"Associez vos appareils de manière permanente\",\n        \"edit-paired-devices_title\": \"Gérer les appareils couplés\",\n        \"join-public-room_title\": \"Rejoindre temporairement la salle publique\",\n        \"cancel-share-mode\": \"Terminé\",\n        \"edit-share-mode\": \"Modifier\",\n        \"expand_title\": \"Agrandir entête bouton ligne\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Déposer pour choisir le destinataire\",\n        \"no-peers-title\": \"Ouvrez PairDrop sur d'autres appareils pour envoyer des fichiers\",\n        \"no-peers-subtitle\": \"Associez des appareils ou entrez dans une salle publique pour être visible sur d'autres réseaux\",\n        \"x-instructions_desktop\": \"Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message\",\n        \"x-instructions_mobile\": \"Appuyez pour envoyer des fichiers ou appuyez longuement pour envoyer un message\",\n        \"x-instructions_data-drop-peer\": \"Déposer pour envoyer au destinataire\",\n        \"x-instructions_data-drop-bg\": \"Lâcher pour choisir le destinataire\",\n        \"x-instructions-share-mode_desktop\": \"Cliquez pour envoyer {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"Appuyez pour envoyer {{descriptor}}\",\n        \"activate-share-mode-base\": \"Ouvrez PairDrop sur d'autres appareils pour envoyer\",\n        \"activate-share-mode-and-other-files-plural\": \"et {{count}} autres fichiers\",\n        \"activate-share-mode-shared-text\": \"texte partagé\",\n        \"activate-share-mode-shared-file\": \"fichier partagé\",\n        \"webrtc-requirement\": \"Pour utiliser cette instance de PairDrop, WebRTC doit être activé !\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} fichiers partagés\",\n        \"activate-share-mode-and-other-file\": \"et un autre fichier\"\n    },\n    \"footer\": {\n        \"known-as\": \"Vous êtes connu comme :\",\n        \"display-name_data-placeholder\": \"Chargement…\",\n        \"display-name_title\": \"Modifiez le nom de votre appareil de manière permanente\",\n        \"discovery\": \"Vous pouvez être découvert :\",\n        \"on-this-network\": \"sur ce réseau\",\n        \"on-this-network_title\": \"Vous pouvez être découvert par tout le monde sur ce réseau.\",\n        \"paired-devices\": \"par les appareils couplés\",\n        \"paired-devices_title\": \"Vous pouvez être découvert par les appareils couplés à tout moment, indépendamment du réseau.\",\n        \"public-room-devices\": \"dans la salle {{roomId}}\",\n        \"public-room-devices_title\": \"Vous pouvez être découvert par les appareils de cette salle publique indépendamment du réseau.\",\n        \"traffic\": \"Le trafic est\",\n        \"routed\": \"routé via le serveur\",\n        \"webrtc\": \"si WebRTC n'est pas disponible.\",\n        \"display-name_placeholder\": \"Chargement…\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"Associer les appareils de manière permanente\",\n        \"input-key-on-this-device\": \"Saisissez cette clé sur un autre appareil\",\n        \"scan-qr-code\": \"ou scannez le QR-code.\",\n        \"enter-key-from-another-device\": \"Entrez ici la clé d'un autre appareil.\",\n        \"temporary-public-room-title\": \"Salle publique temporaire\",\n        \"input-room-id-on-another-device\": \"Saisissez cet ID de salle sur un autre appareil\",\n        \"enter-room-id-from-another-device\": \"Entrez l'ID de la salle depuis un autre appareil pour rejoindre la salle.\",\n        \"hr-or\": \"OU\",\n        \"pair\": \"associer\",\n        \"cancel\": \"Annuler\",\n        \"edit-paired-devices-title\": \"Modifier les appareils couplés\",\n        \"unpair\": \"Dissocier\",\n        \"paired-devices-wrapper_data-empty\": \"Aucun appareil couplé.\",\n        \"auto-accept-instructions-1\": \"Activer\",\n        \"auto-accept\": \"auto-accepter\",\n        \"auto-accept-instructions-2\": \"pour accepter automatiquement tous les fichiers envoyés depuis cet appareil.\",\n        \"close\": \"Fermer\",\n        \"join\": \"Rejoindre\",\n        \"leave\": \"Partir\",\n        \"would-like-to-share\": \"aimerait partager\",\n        \"accept\": \"Accepter\",\n        \"decline\": \"Refuser\",\n        \"has-sent\": \"a envoyé :\",\n        \"share\": \"Partage\",\n        \"download\": \"Télécharger\",\n        \"send-message-title\": \"Envoyer un message\",\n        \"send-message-to\": \"À :\",\n        \"send\": \"Envoyer\",\n        \"receive-text-title\": \"Message reçu\",\n        \"copy\": \"Copier\",\n        \"base64-processing\": \"Traitement…\",\n        \"base64-tap-to-paste\": \"Appuyez ici pour partager {{type}}\",\n        \"base64-paste-to-send\": \"Coller le presse-papiers ici pour partager {{type}}\",\n        \"base64-text\": \"texte\",\n        \"base64-files\": \"fichiers\",\n        \"file-other-description-image\": \"et 1 autre image\",\n        \"file-other-description-file\": \"et 1 autre fichier\",\n        \"file-other-description-image-plural\": \"et {{count}} autres images\",\n        \"file-other-description-file-plural\": \"et {{count}} autres fichiers\",\n        \"title-image\": \"Image\",\n        \"title-file\": \"Fichier\",\n        \"title-image-plural\": \"Images\",\n        \"title-file-plural\": \"Fichiers\",\n        \"receive-title\": \"{{descriptor}} Reçu\",\n        \"download-again\": \"Télécharger à nouveau\",\n        \"language-selector-title\": \"Définir la langue\",\n        \"system-language\": \"Langue du système\",\n        \"message_title\": \"Insérer un message à envoyer\",\n        \"pair-devices-qr-code_title\": \"Cliquer pour copier pour appairer l'appareil\",\n        \"public-room-qr-code_title\": \"Cliquez pour copier le lien vers le salon public\",\n        \"base64-title-text\": \"Texte partagé\",\n        \"paired-device-removed\": \"L'appareil connecté a été enlevé.\",\n        \"message_placeholder\": \"Texte\",\n        \"base64-title-files\": \"Fichiers partagés\",\n        \"approve\": \"approuve\",\n        \"share-text-title\": \"Partage le message\",\n        \"share-text-subtitle\": \"Modifier le message avant l'envoi :\",\n        \"share-text-checkbox\": \"Toujours montrer ce dialogue quand du texte est partagé\",\n        \"close-toast_title\": \"Fermer la notification\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"Fermer à propos de PairDrop\",\n        \"claim\": \"Le moyen le plus simple de transférer des fichiers entre appareils\",\n        \"github_title\": \"PairDrop sur GitHub\",\n        \"buy-me-a-coffee_title\": \"Achetez-moi un café !\",\n        \"tweet_title\": \"Tweet à propos de PairDrop\",\n        \"faq_title\": \"Questions fréquemment posées\",\n        \"bluesky_title\": \"Suis-nous sur BlueSky\",\n        \"custom_title\": \"Suis-nous\",\n        \"privacypolicy_title\": \"Ouvert sur notre politique de confidentialité\",\n        \"mastodon_title\": \"Écrire à propos de PairDrop sur Mastodon\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"Le nom d'affichage est modifié de manière permanente\",\n        \"display-name-changed-temporarily\": \"Le nom d'affichage est modifié uniquement pour cette session\",\n        \"display-name-random-again\": \"Le nom d'affichage est à nouveau généré aléatoirement\",\n        \"download-successful\": \"{{descriptor}} téléchargé\",\n        \"pairing-tabs-error\": \"Le couplage de deux onglets de navigateur Web est impossible\",\n        \"pairing-success\": \"Appareils couplés\",\n        \"pairing-not-persistent\": \"Les appareils couplés ne sont pas persistants\",\n        \"pairing-key-invalid\": \"Clé invalide\",\n        \"pairing-key-invalidated\": \"Clé {{key}} invalidée\",\n        \"pairing-cleared\": \"Tous les appareils ne sont plus appairés\",\n        \"public-room-id-invalid\": \"ID de salle non valide\",\n        \"public-room-left\": \"Salle publique {{publicRoomId}} quittée\",\n        \"copied-to-clipboard\": \"Copié dans le presse-papier\",\n        \"copied-to-clipboard-error\": \"Copie impossible. Copier manuellement.\",\n        \"text-content-incorrect\": \"Le contenu du texte est incorrect\",\n        \"file-content-incorrect\": \"Le contenu du fichier est incorrect\",\n        \"clipboard-content-incorrect\": \"Le contenu du presse-papiers est incorrect\",\n        \"notifications-enabled\": \"Notifications activées\",\n        \"link-received\": \"Lien reçu par {{name}} - Cliquez pour ouvrir\",\n        \"message-received\": \"Message reçu par {{name}} - Cliquez pour copier\",\n        \"click-to-download\": \"Cliquez pour télécharger\",\n        \"request-title\": \"{{name}} souhaite transférer {{count}} {{descriptor}}\",\n        \"click-to-show\": \"Cliquez pour afficher\",\n        \"copied-text\": \"Texte copié dans le presse-papiers\",\n        \"copied-text-error\": \"L'écriture dans le presse-papiers a échoué. Copiez manuellement !\",\n        \"offline\": \"Vous êtes hors ligne\",\n        \"online\": \"Vous êtes de nouveau en ligne\",\n        \"connected\": \"Connecté\",\n        \"online-requirement-pairing\": \"Vous devez être en ligne pour coupler des appareils\",\n        \"online-requirement-public-room\": \"Vous devez être en ligne pour créer une salle publique\",\n        \"connecting\": \"Connexion…\",\n        \"files-incorrect\": \"Les fichiers sont incorrects\",\n        \"file-transfer-completed\": \"Transfert de fichier terminé\",\n        \"ios-memory-limit\": \"L'envoi de fichiers vers iOS n'est possible que jusqu'à 200 Mo à la fois\",\n        \"message-transfer-completed\": \"Transfert de message terminé\",\n        \"unfinished-transfers-warning\": \"Il y a des transferts inachevés. Êtes-vous sûr de vouloir fermer PairDrop ?\",\n        \"rate-limit-join-key\": \"Limite de débit atteinte. Attendez 10 secondes et réessayez.\",\n        \"selected-peer-left\": \"Appareils sélectionnés restants\",\n        \"pair-url-copied-to-clipboard\": \"Lien de couplage de cet appareil copié dans le presse-papier\",\n        \"room-url-copied-to-clipboard\": \"Lien vers la salle publique copié dans le presse-papier\",\n        \"notifications-permissions-error\": \"Permission de notification bloquées car l'utilisateur a plusieurs fois rejeté la demande d'autorisation. Cela peut être réinitialisé via la Page d'Information en cliquant l’icône de cadenas à coté de l'URL.\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Fichier reçu\",\n        \"file-received-plural\": \"{{count}} fichiers reçus\",\n        \"file-transfer-requested\": \"Transfert de fichier demandé\",\n        \"image-transfer-requested\": \"Transfert d'image demandé\",\n        \"message-received\": \"Message reçu\",\n        \"message-received-plural\": \"{{count}} Messages reçus\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Cliquez pour envoyer {{descriptor}}\",\n        \"click-to-send\": \"Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message\",\n        \"connection-hash\": \"Pour vérifier la sécurité du chiffrement de bout en bout, comparez ce numéro de sécurité sur les deux appareils\",\n        \"preparing\": \"Préparation…\",\n        \"waiting\": \"En attente…\",\n        \"processing\": \"En cours…\",\n        \"transferring\": \"Transfert en cours…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/he.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"אודות PairDrop\",\n        \"theme-light_title\": \"השתמש תמיד במצב בהיר\",\n        \"install_title\": \"התקן את PairDrop\",\n        \"edit-share-mode\": \"עריכה\",\n        \"expand_title\": \"הרחב את שורת כפתור הכותרת\",\n        \"language-selector_title\": \"שינוי השפה\",\n        \"about_aria-label\": \"פתח אודות PairDrop\",\n        \"theme-auto_title\": \"התאם את הרקע למערכת באופן אוטומטי\",\n        \"theme-dark_title\": \"השתמש תמיד במצב כהה\",\n        \"notification_title\": \"הפעל התראות\",\n        \"pair-device_title\": \"התאם את המכשירים שלך לתמיד\",\n        \"edit-paired-devices_title\": \"עריכת מכשירים מתואמים\",\n        \"join-public-room_title\": \"הצטרף לחדר ציבורי באופן זמני\",\n        \"cancel-share-mode\": \"ביטול\"\n    },\n    \"instructions\": {\n        \"no-peers-subtitle\": \"תאם מכשירים או היכנס לחדר ציבורי כדי להיות ניתן לגילוי ברשתות אחרות\",\n        \"x-instructions_data-drop-bg\": \"שחרר כדי לבחור נמען\",\n        \"activate-share-mode-and-other-file\": \"וקובץ 1 אחר\",\n        \"activate-share-mode-base\": \"פתח את PairDrop על מכשירים אחרים כדי לשלוח\",\n        \"activate-share-mode-shared-file\": \"קובץ משותף\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} קבצים משותפים\",\n        \"webrtc-requirement\": \"כדי להשתמש בPairdrop, WebRTC מוכרח להיות מופעל!\",\n        \"no-peers_data-drop-bg\": \"שחרר כדי לבחור את הנמען\",\n        \"no-peers-title\": \"פתח את PairDrop במכשירים אחרים כדי לשלוח קבצים\",\n        \"x-instructions_desktop\": \"לחץ כדי לשלוח קבצים או בצע לחיצה ימנית כדי לשלוח הודעה\",\n        \"x-instructions_mobile\": \"גע כדי לשלוח קבצים או בצע נגיעה ארוכה כדי לשלוח הודעה\",\n        \"x-instructions_data-drop-peer\": \"שחרר כדי לשלוח למכשיר\",\n        \"x-instructions-share-mode_desktop\": \"לחץ כדי לשלוח {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"גע כדי לשלוח {{descriptor}}\",\n        \"activate-share-mode-and-other-files-plural\": \"ו{{count}} קבצים אחרים\",\n        \"activate-share-mode-shared-text\": \"טקסט משותף\"\n    },\n    \"footer\": {\n        \"paired-devices_title\": \"הנך ניתן לגילוי על ידי מכשירים מתואמים בכל עת ללא תלות ברשת.\",\n        \"on-this-network\": \"ברשת הזו\",\n        \"on-this-network_title\": \"אתה ניתן לגילוי על ידי כולם ברשת הזו.\",\n        \"display-name_data-placeholder\": \"טוען…\",\n        \"display-name_title\": \"שנה את שם המכשיר שלך לתמיד\",\n        \"known-as\": \"הנך ידוע כ:\",\n        \"discovery\": \"הנך ניתן לגילוי:\",\n        \"paired-devices\": \"על ידי מכשירים מתואמים\",\n        \"public-room-devices\": \"בחדר {{roomId}}\",\n        \"public-room-devices_title\": \"הנך ניתן לגילוי על ידי מכשירים בחדר הציבורי הזה ללא תלות ברשת.\",\n        \"traffic\": \"הנתונים\",\n        \"routed\": \"מנותבים דרך השרת\",\n        \"webrtc\": \"אם WebRTC אינו זמין.\"\n    },\n    \"dialogs\": {\n        \"input-room-id-on-another-device\": \"הזן את מזהה החדר הזה במכשיר אחר\",\n        \"edit-paired-devices-title\": \"ערוך מכשירים מתואמים\",\n        \"paired-device-removed\": \"המכשיר המתואם הוסר.\",\n        \"download-again\": \"הורד שוב\",\n        \"public-room-qr-code_title\": \"לחץ כדי להעתיק את הקישור לחדר הציבורי\",\n        \"auto-accept-instructions-2\": \"כדי לקבל באופן אוטומטי את כל הקבצים שנשלחים ממכשיר זה.\",\n        \"title-file-plural\": \"קבצים\",\n        \"receive-title\": \"{{descriptor}} התקבל\",\n        \"download\": \"הורד\",\n        \"send-message-title\": \"שלח הודעה\",\n        \"message_placeholder\": \"טקסט\",\n        \"receive-text-title\": \"ההודעה התקבלה\",\n        \"base64-text\": \"טקסט\",\n        \"share-text-checkbox\": \"תמיד הצג את חלונית זו כאשר טקסט משותף\",\n        \"system-language\": \"שפת המערכת\",\n        \"title-file\": \"קובץ\",\n        \"pair-devices-title\": \"תאם מכשירים לתמיד\",\n        \"input-key-on-this-device\": \"הזן את הקוד הזה במכשיר אחר\",\n        \"scan-qr-code\": \"או סרוק את הברקוד.\",\n        \"enter-key-from-another-device\": \"הזן את הקוד ממכשיר אחר כאן.\",\n        \"temporary-public-room-title\": \"חדר ציבורי זמני\",\n        \"enter-room-id-from-another-device\": \"הזן מזהה חדר ממכשיר אחר כדי להצטרף לחדר.\",\n        \"hr-or\": \"או\",\n        \"pair\": \"תאם\",\n        \"cancel\": \"ביטול\",\n        \"unpair\": \"בטל התאמה\",\n        \"paired-devices-wrapper_data-empty\": \"אין מכשירים מתואמים.\",\n        \"auto-accept-instructions-1\": \"הפעל\",\n        \"auto-accept\": \"קבלה אוטומטית\",\n        \"close\": \"סגירה\",\n        \"join\": \"הצטרף\",\n        \"leave\": \"עזוב\",\n        \"would-like-to-share\": \"רוצה לשתף\",\n        \"accept\": \"קבל\",\n        \"decline\": \"סרב\",\n        \"has-sent\": \"שלח:\",\n        \"share\": \"שתף\",\n        \"send-message-to\": \"אל:\",\n        \"message_title\": \"הזן את ההודעה לשליחה\",\n        \"send\": \"שלח\",\n        \"copy\": \"העתק\",\n        \"base64-title-files\": \"שתף קבצים\",\n        \"base64-title-text\": \"שתף טקסט\",\n        \"base64-processing\": \"מעבד…\",\n        \"base64-tap-to-paste\": \"גע כאן כדי לשתף {{type}}\",\n        \"base64-paste-to-send\": \"הדבק כאן כדי לשתף {{type}}\",\n        \"base64-files\": \"קבצים\",\n        \"file-other-description-image\": \"ותמונה 1 אחרת\",\n        \"file-other-description-file\": \"וקובץ 1 אחר\",\n        \"file-other-description-image-plural\": \"ו{{count}} תמונות אחרות\",\n        \"file-other-description-file-plural\": \"ו{{count}} קבצים אחרים\",\n        \"title-image\": \"תמונה\",\n        \"title-image-plural\": \"תמונות\",\n        \"language-selector-title\": \"הגדר שפה\",\n        \"pair-devices-qr-code_title\": \"לחץ כדי להעתיק את הקישור כדי לבצע תיאום עם מכשיר זה\",\n        \"approve\": \"אשר\",\n        \"share-text-title\": \"שתף הודעת טקסט\",\n        \"share-text-subtitle\": \"ערוך את ההודעה לפני השליחה:\",\n        \"close-toast_title\": \"סגירת ההתראה\"\n    },\n    \"about\": {\n        \"mastodon_title\": \"כתוב על PairDrop בMastodon\",\n        \"buy-me-a-coffee_title\": \"קנה לי קפה!\",\n        \"claim\": \"הדרך הקלה ביותר להעברת קבצים בין מכשירים\",\n        \"github_title\": \"PairDrop בGitHub\",\n        \"tweet_title\": \"צייץ על PairDrop\",\n        \"custom_title\": \"עקוב אחרינו\",\n        \"bluesky_title\": \"עקוב אחרינו בBlueSky\",\n        \"privacypolicy_title\": \"פתח את מדיניות הפרטיות שלנו\",\n        \"faq_title\": \"שאלות נפוצות\",\n        \"close-about_aria-label\": \"סגירת אודות PairDrop\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"שם התצוגה משתנה לתמיד\",\n        \"download-successful\": \"{{descriptor}} הורד\",\n        \"notifications-permissions-error\": \"ההרשאה להתראות נחסמה עקב סגירת חלונית בקשת ההרשאה מספר פעמים. אתה יכול לאפס זאת במידע על דף זה שניתן לגישה באמצעות לחיצה על אייקון המנעול ליד שורת הכתובת.\",\n        \"click-to-show\": \"לחץ כדי להציג\",\n        \"connecting\": \"מתחבר…\",\n        \"online\": \"הנך מחובר שוב לאינטרנט\",\n        \"pairing-cleared\": \"כל ההתאמות למכשירים הוסרו\",\n        \"pair-url-copied-to-clipboard\": \"הקישור להתאמת המכשיר הזה הועתק\",\n        \"connected\": \"מחובר\",\n        \"online-requirement-pairing\": \"אתה צריך להיות מחובר לאינטרנט כדי לתאם מכשירים\",\n        \"online-requirement-public-room\": \"אתה צריך להיות מחובר כדי ליצור חדר ציבורי\",\n        \"files-incorrect\": \"הקבצים שגויים\",\n        \"unfinished-transfers-warning\": \"יש עוד העברות שלא הסתיימו. אתה בטוח שאתה רוצה לסגור את PairDrop?\",\n        \"rate-limit-join-key\": \"הגעת למגבלה. חכה 10 שניות ונסה שנית.\",\n        \"selected-peer-left\": \"המכשיר הנבחר עזב\",\n        \"display-name-changed-temporarily\": \"שם התצוגה משתנה להפעלה זו בלבד\",\n        \"display-name-random-again\": \"שם התצוגה נוצר שוב באופן אקראי\",\n        \"pairing-tabs-error\": \"תיאום בין שני כרטיסיות בדפדפן אינו אפשרי\",\n        \"pairing-success\": \"המכשירים תואמו\",\n        \"pairing-not-persistent\": \"המכשירים המתואמים אינם קבועים\",\n        \"pairing-key-invalid\": \"קוד שגוי\",\n        \"pairing-key-invalidated\": \"הקוד {{key}} לא תקף עוד\",\n        \"public-room-id-invalid\": \"מזהה חדר שגוי\",\n        \"public-room-left\": \"עזבת את החדר הציבורי {{publicRoomId}}\",\n        \"copied-to-clipboard\": \"הועתק\",\n        \"room-url-copied-to-clipboard\": \"הקישור לחדר הציבורי הועתק\",\n        \"copied-to-clipboard-error\": \"העתקה אינה אפשרית. העתק ידנית.\",\n        \"text-content-incorrect\": \"תוכן הטקסט שגוי\",\n        \"file-content-incorrect\": \"תוכן הקובץ שגוי\",\n        \"clipboard-content-incorrect\": \"התוכן שהודבק שגוי\",\n        \"notifications-enabled\": \"ההתראות אופשרו\",\n        \"link-received\": \"קישור התקבל מ{{name}} - לחץ כדי לפתוח\",\n        \"message-received\": \"הודעה התקבלה מ{{name}} - לחץ כדי להעתיק\",\n        \"click-to-download\": \"לחץ כדי להוריד\",\n        \"request-title\": \"{{name}} רוצה לשלוח {{count}} {{descriptor}}\",\n        \"copied-text\": \"הטקסט הועתק\",\n        \"copied-text-error\": \"ההעתקה נכשלה. העתק ידנית!\",\n        \"offline\": \"הנך לא מחובר לאינטרנט\",\n        \"file-transfer-completed\": \"העברת הקובץ הושלמה בהצלחה\",\n        \"ios-memory-limit\": \"שליחת קבצים למכשירי iOS אפשרית רק עד 200 MB בבת אחת\",\n        \"message-transfer-completed\": \"העברת ההודעה הושלמה בהצלחה\"\n    },\n    \"peer-ui\": {\n        \"click-to-send\": \"לחץ כדי לשלוח קבצים או בצע לחיצה ימנית כדי לשלוח הודעה\",\n        \"click-to-send-share-mode\": \"לחץ כדי לשלוח {{descriptor}}\",\n        \"connection-hash\": \"כדי לאמת את האבטחה של ההצפנה מצד לצד, השווה את מספר אבטחה זה בין שני המכשירים\",\n        \"preparing\": \"מתכונן…\",\n        \"waiting\": \"מחכה…\",\n        \"processing\": \"מעבד…\",\n        \"transferring\": \"מעביר…\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"הקובץ התקבל\",\n        \"file-received-plural\": \"{{count}} קבצים התקבלו\",\n        \"file-transfer-requested\": \"העברת קבצים מתבקשת\",\n        \"image-transfer-requested\": \"העברת תמונות מתבקשת\",\n        \"message-received\": \"התקבלה הודעה\",\n        \"message-received-plural\": \"{{count}} הודעות התקבלו\"\n    }\n}\n"
  },
  {
    "path": "public/lang/hu.json",
    "content": "{\n    \"header\": {\n        \"language-selector_title\": \"Nyelv beállítása\",\n        \"theme-auto_title\": \"Téma automatikusan rendszerhez igazítása\",\n        \"about_aria-label\": \"\\\"A PairDrop-ról\\\" megnyitása\",\n        \"theme-light_title\": \"Mindig világos téma használata\",\n        \"theme-dark_title\": \"Mindig sötét téma használata\",\n        \"notification_title\": \"Értesítések engedélyezése\",\n        \"install_title\": \"PairDrop telepítése\",\n        \"edit-paired-devices_title\": \"Párosított eszközök szerkeztése\",\n        \"cancel-share-mode\": \"Mégse\",\n        \"edit-share-mode\": \"Szerkesztés\",\n        \"expand_title\": \"Fejléc gombsorának kibővítése\",\n        \"about_title\": \"A PairDrop-ról\",\n        \"pair-device_title\": \"Eszközei végleges párosítása\",\n        \"join-public-room_title\": \"Ideiglenes csatlakozás a nyilvános szobához\"\n    },\n    \"instructions\": {\n        \"no-peers-title\": \"Nyissa meg a PairDrop-ot más eszközökön a fájlok küldéséhez\",\n        \"no-peers_data-drop-bg\": \"Engedje el a címzett kiválasztásához\",\n        \"x-instructions_desktop\": \"Kattintson a fájlküldéshez, vagy kattintson jobb gombbal üzenet küldéséhez\",\n        \"x-instructions_data-drop-peer\": \"Engedje el a partnernek való küldéshez\",\n        \"x-instructions_data-drop-bg\": \"Engedje el a címzett kiválasztásához\",\n        \"activate-share-mode-base\": \"Nyissa meg a PairDrop-ot más eszközökön a küldéséhez\",\n        \"activate-share-mode-and-other-file\": \"és egy másik fájl\",\n        \"activate-share-mode-and-other-files-plural\": \"és {{count}} másik fájl\",\n        \"activate-share-mode-shared-file\": \"megosztott fájl\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} megosztott fájl\",\n        \"x-instructions_mobile\": \"Koppintson a fájlküldéshez, vagy nyomja hosszan üzenet küldéséhez\",\n        \"x-instructions-share-mode_desktop\": \"Kattintson a {{descriptor}} küldéséhez\",\n        \"x-instructions-share-mode_mobile\": \"Koppintson a {{descriptor}} küldéséhez\",\n        \"activate-share-mode-shared-text\": \"megosztott szöveg\",\n        \"webrtc-requirement\": \"Ezen PairDrop példány használatához engedélyezni kell a WebRTC-t!\",\n        \"no-peers-subtitle\": \"Párosítsa eszközeit vagy lépjen be egy nyilvános szobába, hogy más hálózatokon is felfedezhető legyen\"\n    },\n    \"footer\": {\n        \"known-as\": \"Ön így látható:\",\n        \"display-name_data-placeholder\": \"Betöltés…\",\n        \"display-name_title\": \"Az eszköze nevének végleges megváltoztatása\",\n        \"discovery\": \"Ön felfedezhető:\",\n        \"on-this-network_title\": \"Mindenki által felfedezhető a hálózaton.\",\n        \"paired-devices\": \"a csatlakoztatott eszközök által\",\n        \"public-room-devices\": \"a {{roomId}} szobában\",\n        \"traffic\": \"A forgalom\",\n        \"routed\": \"a szerveren van átirányítva\",\n        \"webrtc\": \"ha nem elérhető a WebRTC.\",\n        \"on-this-network\": \"ezen a hálózaton\",\n        \"paired-devices_title\": \"A párosított eszközeid által minden esetben felfedezhető a hálózaton, a hálózattól függetlenül.\",\n        \"public-room-devices_title\": \"Mindenki által felfedezhető ebben a nyilvános szobában, a hálózattól függetlenül.\"\n    },\n    \"dialogs\": {\n        \"input-key-on-this-device\": \"Írja be ezt a kódot egy másik eszközön\",\n        \"pair-devices-title\": \"Eszközök végleges párosítása\",\n        \"temporary-public-room-title\": \"Ideiglenes nyilvános szoba\",\n        \"input-room-id-on-another-device\": \"Írja be ezt a szobakódot egy másik eszközön\",\n        \"enter-room-id-from-another-device\": \"Írja be a szobakódot egy másik eszközről a szobához való csatlakozáshoz.\",\n        \"cancel\": \"Mégse\",\n        \"pair\": \"Párosítás\",\n        \"edit-paired-devices-title\": \"Párosított eszközök szerkesztése\",\n        \"unpair\": \"Párosítás megszüntetése\",\n        \"paired-device-removed\": \"Párosított eszköz eltávolítva.\",\n        \"paired-devices-wrapper_data-empty\": \"Nincs párosított eszköz.\",\n        \"auto-accept\": \"automatikus elfogadás\",\n        \"close\": \"Bezárás\",\n        \"join\": \"Csatlakozás\",\n        \"would-like-to-share\": \"szeretne megosztani\",\n        \"accept\": \"Elfogadás\",\n        \"has-sent\": \"küldött:\",\n        \"share\": \"Megosztás\",\n        \"send-message-title\": \"Üzenet küldése\",\n        \"send-message-to\": \"Neki:\",\n        \"receive-text-title\": \"Üzenet érkezett\",\n        \"base64-processing\": \"Feldolgozás…\",\n        \"scan-qr-code\": \"vagy olvassa be a QR-kódot.\",\n        \"auto-accept-instructions-1\": \"Aktiválás\",\n        \"auto-accept-instructions-2\": \"hogy automatikusan elfogadja az adott eszközről küldött összes fájlt.\",\n        \"leave\": \"Kilépés\",\n        \"decline\": \"Elutasítás\",\n        \"download\": \"Letöltés\",\n        \"send\": \"Küldés\",\n        \"message_title\": \"Írja be a küldeni kívánt üzenetet\",\n        \"message_placeholder\": \"Szöveg\",\n        \"copy\": \"Másolás\",\n        \"base64-title-files\": \"Fájlok megosztása\",\n        \"base64-title-text\": \"Szöveg megosztása\",\n        \"enter-key-from-another-device\": \"Ide írja be a kódot egy másik eszközről.\",\n        \"hr-or\": \"VAGY\",\n        \"base64-text\": \"szöveg\",\n        \"base64-tap-to-paste\": \"Koppintson ide a {{type}} megosztásához\",\n        \"base64-paste-to-send\": \"Illessze be a ide a vágólapja tartalmát a {{type}} megosztásához\",\n        \"base64-files\": \"fájlok\",\n        \"file-other-description-image\": \"és egy másik kép\",\n        \"file-other-description-file\": \"és egy másik fájl\",\n        \"file-other-description-image-plural\": \"és {{count}} másik kép\",\n        \"file-other-description-file-plural\": \"és {{count}} másik fájl\",\n        \"title-image\": \"Kép\",\n        \"title-file\": \"Fájl\",\n        \"title-image-plural\": \"Képek\",\n        \"title-file-plural\": \"Fájlok\",\n        \"receive-title\": \"{{descriptor}} érkezett\",\n        \"download-again\": \"Letöltés újra\",\n        \"language-selector-title\": \"Nyelv beállítása\",\n        \"system-language\": \"Rendszer nyelve\",\n        \"approve\": \"jóváhagyás\",\n        \"share-text-title\": \"Szöveges üzenet megosztása\",\n        \"share-text-subtitle\": \"Üzenet szerkesztése küldés előtt:\",\n        \"share-text-checkbox\": \"Mindig mutassa ezt a párbeszédablakot üzenet megosztásakor\",\n        \"close-toast_title\": \"Értesítés bezárása\",\n        \"public-room-qr-code_title\": \"Kattintson a szoba linkjének másolásához\",\n        \"pair-devices-qr-code_title\": \"Kattintson az eszköz párosításához való link másolásához\"\n    },\n    \"notifications\": {\n        \"online-requirement-pairing\": \"Online kell lennie készülékek párosításához\",\n        \"link-received\": \"Link érkezett tőle: {{name}} - Kattintson a megnyitáshoz\",\n        \"selected-peer-left\": \"A kiválasztott partner kilépett\",\n        \"display-name-changed-permanently\": \"A megjelenített neved véglegesen meg lett változtatva\",\n        \"download-successful\": \"{{descriptor}} letöltve\",\n        \"pairing-tabs-error\": \"Két böngészőlap párosítása lehetetlen\",\n        \"display-name-random-again\": \"A megjelenített neved véletlenszerűen újra lett generálva\",\n        \"pairing-key-invalid\": \"Érvénytelen kulcs\",\n        \"pairing-cleared\": \"Minden eszköz párosítása megszüntetve\",\n        \"public-room-id-invalid\": \"Érvénytelen szobakód\",\n        \"copied-to-clipboard\": \"Vágólapra másolva\",\n        \"copied-to-clipboard-error\": \"Másolás nem lehetséges. Manuális másolás szükséges.\",\n        \"text-content-incorrect\": \"A szöveg tartalma helytelen\",\n        \"file-content-incorrect\": \"A fájl tartalma helytelen\",\n        \"pair-url-copied-to-clipboard\": \"Az eszköz párosítására való link másolva a vágólapra\",\n        \"notifications-enabled\": \"Értesítések engedélyezve\",\n        \"request-title\": \"{{name}} szeretne küldeni: {{count}} {{descriptor}}\",\n        \"copied-text-error\": \"A vágólapra való írás nem sikerült. Manuális másolás szükséges.\",\n        \"click-to-download\": \"Kattintson a letöltéshez\",\n        \"click-to-show\": \"Kattintson a megjelenítéshez\",\n        \"connected\": \"Csatlakoztatva\",\n        \"online\": \"Újra online lett\",\n        \"connecting\": \"Csatlakozás…\",\n        \"files-incorrect\": \"A fájlok helytelenek\",\n        \"file-transfer-completed\": \"A fájlok küldése sikeres\",\n        \"message-transfer-completed\": \"Üzenet küldése sikeres\",\n        \"unfinished-transfers-warning\": \"Befejezetlen átvitelek folyamatban. Biztosan be akarja zárni a PairDrop-ot?\",\n        \"pairing-success\": \"Eszközök párosítva\",\n        \"pairing-not-persistent\": \"A párosított eszközök nem maradandóak\",\n        \"pairing-key-invalidated\": \"{{key}} kulcs érvénytelenítve\",\n        \"public-room-left\": \"{{publicRoomId}} szoba elhagyva\",\n        \"online-requirement-public-room\": \"Online kell lennie szoba készítéséhez\",\n        \"ios-memory-limit\": \"Egyszerre csak 200 MB-os fájlátvitel lehetséges iOS-re való küldéskor\",\n        \"room-url-copied-to-clipboard\": \"A nyilvános szoba linkje másolva a vágólapra\",\n        \"clipboard-content-incorrect\": \"A vágólap tartalma helytelen\",\n        \"copied-text\": \"Szöveg másolva a vágólapra\",\n        \"message-received\": \"Üzenet érkezett tőle: {{name}} - Kattintson a másoláshoz\",\n        \"notifications-permissions-error\": \"Az értesítések engedélye letiltásra került, mivel a felhasználó többször is elutasította az engedélykérést. Ez visszaállítható az oldalinformációkban, amely az URL-sáv melletti lakat ikonra kattintva érhet el.\",\n        \"offline\": \"Ön offline\",\n        \"rate-limit-join-key\": \"Az átviteli sebességhatár elérte a határt. Várjon 10 másodpercet, és próbálja meg újra.\",\n        \"display-name-changed-temporarily\": \"A megjelenített neved meg lett változtatva csak erre a munkamenetre\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"A PairDrop-ról bezárása\",\n        \"github_title\": \"PairDrop a GitHub-on\",\n        \"buy-me-a-coffee_title\": \"Vegyél nekem egy kávét!\",\n        \"mastodon_title\": \"Írj a PairDrop-ról Mastodon-on\",\n        \"bluesky_title\": \"Kövess minket a BlueSky-on\",\n        \"faq_title\": \"Gyakran ismételt kérdések\",\n        \"claim\": \"Az eszközök közötti fájlátvitel legegyszerűbb módja\",\n        \"tweet_title\": \"Tweetelj a PairDrop-ról\",\n        \"custom_title\": \"Kövess minket\",\n        \"privacypolicy_title\": \"Nyisd meg az adatvédelmi szabályzatunkat\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Fájl érkezett\",\n        \"file-received-plural\": \"{{count}} fájl érkezett\",\n        \"image-transfer-requested\": \"Képátvitel kérelmezve\",\n        \"message-received\": \"Üzenet érkezett\",\n        \"message-received-plural\": \"{{count}} üzenet érkezett\",\n        \"file-transfer-requested\": \"Fájlátvitel kérelmezve\"\n    },\n    \"peer-ui\": {\n        \"preparing\": \"Előkészítés…\",\n        \"click-to-send-share-mode\": \"Kattintson a {{descriptor}} küldéséhez\",\n        \"waiting\": \"Várakozás…\",\n        \"processing\": \"Feldolgozás…\",\n        \"transferring\": \"Átvitel…\",\n        \"click-to-send\": \"Kattintson fájlok küldéséhez vagy kattintson jobb gombbal üzenet küldéséhez\",\n        \"connection-hash\": \"A végpontok közötti titkosítás biztonságának ellenőrzéséhez hasonlítsa össze ezt a biztonsági számot mindkét eszközön\"\n    }\n}\n"
  },
  {
    "path": "public/lang/id.json",
    "content": "{\n    \"footer\": {\n        \"webrtc\": \"jika WebRTC tidak tersedia.\",\n        \"public-room-devices_title\": \"Anda dapat ditemukan oleh perangkat di ruang publik ini terlepas dari jaringan.\",\n        \"display-name_data-placeholder\": \"Memuat…\",\n        \"display-name_title\": \"Edit nama perangkat Anda scr. permanen\",\n        \"traffic\": \"Lalu lintas\",\n        \"paired-devices_title\": \"Anda dapat ditemukan oleh perangkat yang dipasangkan setiap saat tergantung pada jaringan.\",\n        \"public-room-devices\": \"di dalam ruang {{roomId}}\",\n        \"paired-devices\": \"pada prngkt. yg. dipasangkan\",\n        \"on-this-network\": \"pada jaringan ini\",\n        \"routed\": \"diarahkan melalui server\",\n        \"discovery\": \"Anda dapat ditemukan:\",\n        \"on-this-network_title\": \"Anda dapat ditemukan oleh semua orang di jaringan ini.\",\n        \"known-as\": \"Anda dikenal sebagai:\"\n    },\n    \"notifications\": {\n        \"request-title\": \"{{name}} ingin mentransfer {{count}} {{descriptor}}\",\n        \"unfinished-transfers-warning\": \"Ada transfer yang belum selesai. Apakah Anda yakin ingin menutup PairDrop?\",\n        \"message-received\": \"Pesan diterima dari {{name}} - Klik untuk menyalin\",\n        \"rate-limit-join-key\": \"Batasan tercapai. Tunggu 10 detik dan coba lagi.\",\n        \"connecting\": \"Menghubungkan…\",\n        \"pairing-key-invalidated\": \"Kunci {{key}} takvalid\",\n        \"pairing-key-invalid\": \"Kunci takvalid\",\n        \"connected\": \"Tersambung\",\n        \"pairing-not-persistent\": \"Perangkat dipasangkan tidak akan bertahan lama\",\n        \"text-content-incorrect\": \"Isi teks keliru\",\n        \"message-transfer-completed\": \"Transfer pesan selesai\",\n        \"file-transfer-completed\": \"Transfer berkas selesai\",\n        \"file-content-incorrect\": \"Isi berkas keliru\",\n        \"files-incorrect\": \"Berkas tidak benar\",\n        \"selected-peer-left\": \"Rekan terpilih keluar\",\n        \"link-received\": \"Tautan diterima dari {{name}} - Klik untuk membuka\",\n        \"online\": \"Anda kembali online\",\n        \"public-room-left\": \"Keluar dari ruang publik {{publicRoomId}}\",\n        \"copied-text\": \"Teks disalin ke papan klip\",\n        \"display-name-random-again\": \"Nama tampilan dibuat secara acak lagi\",\n        \"display-name-changed-permanently\": \"Nama tampilan diubah secara permanen\",\n        \"copied-to-clipboard-error\": \"Penyalinan tak dapat dilakukan. Salinlah secara manual.\",\n        \"pairing-success\": \"Perangkat dipasangkan\",\n        \"clipboard-content-incorrect\": \"Isi papan klip keliru\",\n        \"display-name-changed-temporarily\": \"Nama tampilan hanya diubah untuk sesi ini\",\n        \"copied-to-clipboard\": \"Disalin ke papan klip\",\n        \"offline\": \"Anda sedang offline\",\n        \"pairing-tabs-error\": \"Memasangkan dua tab browser web tidak mungkin dilakukan\",\n        \"public-room-id-invalid\": \"ID Ruang takvalid\",\n        \"click-to-download\": \"Klik untuk mengunduh\",\n        \"pairing-cleared\": \"Semua Perangkat dilepaskan\",\n        \"notifications-enabled\": \"Notifikasi diaktifkan\",\n        \"online-requirement-pairing\": \"Anda harus online untuk memasangkan perangkat\",\n        \"ios-memory-limit\": \"Mengirim betkas ke iOS hanya dapat dilakukan hingga 200 MB sekaligus\",\n        \"online-requirement-public-room\": \"Anda harus terhubung ke jaringan internet untuk membuat ruang publik\",\n        \"copied-text-error\": \"Menyalin ke papan klip gagal. Salinlah secara manual!\",\n        \"download-successful\": \"{{descriptor}} diunduh\",\n        \"click-to-show\": \"Klik untuk menampilkan\",\n        \"notifications-permissions-error\": \"Izin pemberitahuan telah diblokir karena pengguna telah mengabaikan permintaan izin beberapa kali. Hal ini dapat diatur ulang di Info Halaman yang dapat diakses dengan mengeklik ikon kunci di sebelah bilah URL.\",\n        \"pair-url-copied-to-clipboard\": \"Tautan untuk memasangkan perangkat ini disalin ke papan klip\",\n        \"room-url-copied-to-clipboard\": \"Tautan ke ruang publik disalin ke papan klip\"\n    },\n    \"header\": {\n        \"cancel-share-mode\": \"Batalkan\",\n        \"theme-auto_title\": \"Sesuaikan tema dengan sistem\",\n        \"install_title\": \"Instal PairDrop\",\n        \"theme-dark_title\": \"Selalu gunakan tema gelap\",\n        \"pair-device_title\": \"Pasangkan perangkat anda secara permanen\",\n        \"join-public-room_title\": \"Bergabung dgn. ruang publik sementara\",\n        \"notification_title\": \"Aktifkan notifikasi\",\n        \"edit-paired-devices_title\": \"Edit perangkat yg. dipasangkan\",\n        \"language-selector_title\": \"Atur Bahasa\",\n        \"about_title\": \"Tentang PairDrop\",\n        \"about_aria-label\": \"Buka Tentang PairDrop\",\n        \"theme-light_title\": \"Selalu gunakan tema terang\",\n        \"edit-share-mode\": \"Sunting\",\n        \"expand_title\": \"Perluas baris tombol header\"\n    },\n    \"instructions\": {\n        \"x-instructions_mobile\": \"Ketuk untuk mengirim berkas atau ketuk lama untuk mengirim pesan\",\n        \"x-instructions-share-mode_desktop\": \"Klik untuk mengirim {{descriptor}}\",\n        \"activate-share-mode-and-other-files-plural\": \"dan {{count}} berkas lainnya\",\n        \"x-instructions-share-mode_mobile\": \"Ketuk untuk mengirim {{descriptor}}\",\n        \"activate-share-mode-base\": \"Buka PairDrop di perangkat lain untuk berkirim\",\n        \"no-peers-subtitle\": \"Pasangkan perangkat atau masuk ke ruang publik agar dapat terdeteksi di jaringan lain\",\n        \"activate-share-mode-shared-text\": \"teks bersama\",\n        \"x-instructions_desktop\": \"Klik untuk mengirim berkas atau klik kanan untuk mengirim pesan\",\n        \"no-peers-title\": \"Buka PairDrop di perangkat lain untuk berkirim berkas\",\n        \"x-instructions_data-drop-peer\": \"Lepaskan untuk mengirim ke rekan\",\n        \"x-instructions_data-drop-bg\": \"Lepaskan untuk memilih penerima\",\n        \"no-peers_data-drop-bg\": \"Lepaskan untuk memilih penerima\",\n        \"activate-share-mode-and-other-file\": \"dan 1 berkas lainnya\",\n        \"activate-share-mode-shared-file\": \"berkas yang dibagikan\",\n        \"activate-share-mode-shared-files-plural\": \"sebanyak {{count}} berkas dibagikan\",\n        \"webrtc-requirement\": \"Untuk menggunakan instance PairDrop ini, WebRTC harus diaktifkan!\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"Memproses…\",\n        \"click-to-send-share-mode\": \"Klik untuk mengirim {{descriptor}}\",\n        \"click-to-send\": \"Klik untuk mengirim berkas atau klik kanan untuk mengirim pesan\",\n        \"waiting\": \"Menunggu…\",\n        \"connection-hash\": \"Untuk memverifikasi keamanan enkripsi end-to-end, bandingkan nomor keamanan ini pada kedua perangkat\",\n        \"preparing\": \"Menyiapkan…\",\n        \"transferring\": \"Mentransfer…\"\n    },\n    \"dialogs\": {\n        \"base64-paste-to-send\": \"Tempel salinan di sini untuk mengirim {{type}}\",\n        \"auto-accept-instructions-2\": \"untuk menerima semua berkas yang dikirim dari perangkat tersebut secara otomatis.\",\n        \"receive-text-title\": \"Pesan Diterima\",\n        \"edit-paired-devices-title\": \"Edit Perangkat yg. Dipasangkan\",\n        \"cancel\": \"Batal\",\n        \"auto-accept-instructions-1\": \"Aktifkan\",\n        \"pair-devices-title\": \"Pasangkan Perangkat Scr. Permanen\",\n        \"download\": \"Unduh\",\n        \"title-file\": \"Berkas\",\n        \"base64-processing\": \"Memproses…\",\n        \"decline\": \"Tolak\",\n        \"receive-title\": \"{{descriptor}} Diterima\",\n        \"leave\": \"Tinggalkan\",\n        \"join\": \"Gabung\",\n        \"title-image-plural\": \"Gambar\",\n        \"send\": \"Kirim\",\n        \"base64-tap-to-paste\": \"Ketuk di sini untuk membagikan{{type}}\",\n        \"base64-text\": \"teks\",\n        \"copy\": \"Salin\",\n        \"file-other-description-image\": \"dan 1 gambar lainnya\",\n        \"temporary-public-room-title\": \"Ruang Publik Sementara\",\n        \"base64-files\": \"berkas\",\n        \"has-sent\": \"telah mengirim:\",\n        \"file-other-description-file\": \"dan 1 berkas lainnya\",\n        \"close\": \"Tutup\",\n        \"system-language\": \"Bahasa Sistem\",\n        \"unpair\": \"Lepas\",\n        \"title-image\": \"Gambar\",\n        \"file-other-description-file-plural\": \"dan {{count}} berkas lainnya\",\n        \"would-like-to-share\": \"ingin berbagi\",\n        \"send-message-to\": \"Ke:\",\n        \"language-selector-title\": \"Pilih Bahasa\",\n        \"pair\": \"Pasangkan\",\n        \"hr-or\": \"ATAU\",\n        \"scan-qr-code\": \"atau pindai kode QR.\",\n        \"input-key-on-this-device\": \"Masukkan kunci ini pada perangkat lain\",\n        \"download-again\": \"Unduh lagi\",\n        \"accept\": \"Terima\",\n        \"paired-devices-wrapper_data-empty\": \"Tak ada perangkat yg. dipasangkan.\",\n        \"enter-key-from-another-device\": \"Masukkan kunci dari perangkat lain di sini.\",\n        \"share\": \"Bagikan\",\n        \"auto-accept\": \"terima scr. otomatis\",\n        \"title-file-plural\": \"Berkas\",\n        \"send-message-title\": \"Kirim Pesan\",\n        \"input-room-id-on-another-device\": \"Masukkan ID ruang ini pada perangkat lain\",\n        \"file-other-description-image-plural\": \"dan {{count}} gambar lainnya\",\n        \"enter-room-id-from-another-device\": \"Masukkan ID ruang dari perangkat lain untuk bergabung.\",\n        \"message_title\": \"Masukkan pesan untuk dikirim\",\n        \"pair-devices-qr-code_title\": \"Klik untuk menyalin tautan untuk memasangkan perangkat ini\",\n        \"public-room-qr-code_title\": \"Klik untuk menyalin tautan ke ruang publik\",\n        \"base64-title-files\": \"Bagikan Berkas\",\n        \"base64-title-text\": \"Bagikan Teks\",\n        \"message_placeholder\": \"Teks\",\n        \"paired-device-removed\": \"Perangkat yang dipasangkan telah dihapus.\",\n        \"approve\": \"setujui\",\n        \"share-text-title\": \"Bagikan Pesan Teks\",\n        \"share-text-subtitle\": \"Ubah sebelum pesan dikirimkan:\",\n        \"share-text-checkbox\": \"Selalu tampilkan dialog ketika membagikan teks\",\n        \"close-toast_title\": \"Tutup notifikasi\"\n    },\n    \"about\": {\n        \"claim\": \"Cara termudah untuk mentransfer berkas lintas perangkat\",\n        \"tweet_title\": \"Tweet tentang PairDrop\",\n        \"close-about_aria-label\": \"Tutup Tentang PairDrop\",\n        \"buy-me-a-coffee_title\": \"Traktir aku kopi!\",\n        \"github_title\": \"PairDrop di GitHub\",\n        \"faq_title\": \"Pertanyaan yang sering diajukan\",\n        \"mastodon_title\": \"Tulis tentang PairDrop di Mastodon\",\n        \"bluesky_title\": \"Ikuti kami di BlueSky\",\n        \"custom_title\": \"Ikuti kami\",\n        \"privacypolicy_title\": \"Buka kebijakan privasi kami\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"Permintaan Pentransferan Berkas\",\n        \"message-received-plural\": \"{{count}} Pesan Diterima\",\n        \"message-received\": \"Pesan Diterima\",\n        \"file-received\": \"Berkas Diterima\",\n        \"file-received-plural\": \"Sebanyak {{count}} Berkas Diterima\",\n        \"image-transfer-requested\": \"Permintaan Transfer Gambar\"\n    }\n}\n"
  },
  {
    "path": "public/lang/it.json",
    "content": "{\n    \"footer\": {\n        \"webrtc\": \"se WebRTC non è disponibile.\",\n        \"public-room-devices_title\": \"Puoi essere rilevato dai dispositivi presenti in questa stanza pubblica indipendentemente dalla rete.\",\n        \"display-name_data-placeholder\": \"Caricamento…\",\n        \"display-name_title\": \"Modifica il nome del tuo dispositivo permanentemente\",\n        \"traffic\": \"Il traffico è\",\n        \"paired-devices_title\": \"Puoi essere rilevato dai dispositivi associati in ogni momento, indipendentemente dalla rete.\",\n        \"public-room-devices\": \"nella stanza {{roomId}}\",\n        \"paired-devices\": \"da dispositivi associati\",\n        \"on-this-network\": \"su questa rete\",\n        \"routed\": \"instradato attraverso il server\",\n        \"discovery\": \"Puoi essere rilevato:\",\n        \"on-this-network_title\": \"puoi essere rilevato da chiunque su questa rete.\",\n        \"known-as\": \"Sei visibile come:\"\n    },\n    \"header\": {\n        \"cancel-share-mode\": \"Annulla\",\n        \"theme-auto_title\": \"Adatta il tema al sistema automaticamente\",\n        \"install_title\": \"Installa PairDrop\",\n        \"theme-dark_title\": \"Usa sempre il tema scuro\",\n        \"pair-device_title\": \"Abbina i tuoi dispositivi permanentemente\",\n        \"join-public-room_title\": \"Unisciti ad una stanza pubblica temporaneamente\",\n        \"notification_title\": \"Attiva notifiche\",\n        \"edit-paired-devices_title\": \"Modifica i dispositivi abbinati\",\n        \"language-selector_title\": \"Imposta Lingua\",\n        \"about_title\": \"Informazioni su PairDrop\",\n        \"about_aria-label\": \"Apri informazioni su PairDrop\",\n        \"theme-light_title\": \"Usa sempre il tema chiaro\",\n        \"edit-share-mode\": \"Modifica\",\n        \"expand_title\": \"Espandi la riga dei pulsanti nell'intestazione\"\n    },\n    \"instructions\": {\n        \"x-instructions_mobile\": \"Tocca per inviare file o tocco prolungato per inviare un messaggio\",\n        \"x-instructions-share-mode_desktop\": \"Clicca per inviare {{descriptor}}\",\n        \"activate-share-mode-and-other-files-plural\": \"e altri {{count}} files\",\n        \"x-instructions-share-mode_mobile\": \"Tocca per inviare {{descriptor}}\",\n        \"activate-share-mode-base\": \"Apri PairDrop su altri dispositivi per inviare\",\n        \"no-peers-subtitle\": \"Abbina dispositivi o entra in una stanza pubblica per essere rilevabile su altre reti\",\n        \"activate-share-mode-shared-text\": \"testo condiviso\",\n        \"x-instructions_desktop\": \"Clicca per inviare files o usa il tasto destro per inviare un messaggio\",\n        \"no-peers-title\": \"Apri PairDrop su altri dispositivi per inviare files\",\n        \"x-instructions_data-drop-peer\": \"Rilascia per inviare al peer\",\n        \"x-instructions_data-drop-bg\": \"Rilascia per selezionare il destinatario\",\n        \"no-peers_data-drop-bg\": \"Rilascia per selezionare il destinatario\",\n        \"webrtc-requirement\": \"Per usare questa istanza di PairDrop, devi attivare WebRTC!\",\n        \"activate-share-mode-shared-file\": \"file condiviso\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} files condivisi\",\n        \"activate-share-mode-and-other-file\": \"ed 1 altro file\"\n    },\n    \"dialogs\": {\n        \"auto-accept-instructions-2\": \"per accettare automaticamente tutti i files inviati da quel dispositivo.\",\n        \"edit-paired-devices-title\": \"Modifica Dispositivi Associati\",\n        \"cancel\": \"Annulla\",\n        \"auto-accept-instructions-1\": \"Attiva\",\n        \"pair-devices-title\": \"Associa Dispositivi Permanentemente\",\n        \"temporary-public-room-title\": \"Stanza Pubblica Temporanea\",\n        \"close\": \"Chiudi\",\n        \"unpair\": \"Dissocia\",\n        \"pair\": \"Associa\",\n        \"scan-qr-code\": \"o scannerizza il codice QR.\",\n        \"input-key-on-this-device\": \"Inserisci questo codice su un altro dispositivo\",\n        \"paired-devices-wrapper_data-empty\": \"Nessun dispositivo associato.\",\n        \"enter-key-from-another-device\": \"Inserisci il codice dell'altro dispositivo qui.\",\n        \"auto-accept\": \"accetta automaticamente\",\n        \"input-room-id-on-another-device\": \"Inserisci l'ID di questa stanza su un altro dispositivo\",\n        \"enter-room-id-from-another-device\": \"Inserisci l'ID stanza da un altro dispositivo per accedere alla stanza.\",\n        \"base64-paste-to-send\": \"Incolla qui per inviare {{type}}\",\n        \"receive-text-title\": \"Messaggio Ricevuto\",\n        \"download\": \"Scarica\",\n        \"title-file\": \"File\",\n        \"base64-processing\": \"Elaborazione…\",\n        \"decline\": \"Rifiuta\",\n        \"receive-title\": \"{{descriptor}} Ricevuto\",\n        \"leave\": \"Abbandona\",\n        \"join\": \"Unisciti\",\n        \"title-image-plural\": \"Immagini\",\n        \"send\": \"Invia\",\n        \"base64-tap-to-paste\": \"Tocca qui per condividere {{type}}\",\n        \"base64-text\": \"testo\",\n        \"copy\": \"Copia\",\n        \"file-other-description-image\": \"e 1 altra immagine\",\n        \"base64-files\": \"files\",\n        \"has-sent\": \"ha inviato:\",\n        \"file-other-description-file\": \"ed 1 altro file\",\n        \"system-language\": \"Lingua di Sistema\",\n        \"title-image\": \"Immagine\",\n        \"file-other-description-file-plural\": \"e altri {{count}} files\",\n        \"would-like-to-share\": \"vorrebbe condividere\",\n        \"send-message-to\": \"A:\",\n        \"language-selector-title\": \"Imposta Lingua\",\n        \"hr-or\": \"OPPURE\",\n        \"download-again\": \"Scarica ancora\",\n        \"accept\": \"Accetta\",\n        \"share\": \"Condividi\",\n        \"title-file-plural\": \"Files\",\n        \"send-message-title\": \"Invia Messaggio\",\n        \"file-other-description-image-plural\": \"e {{count}} altre immagini\",\n        \"message_title\": \"Inserire messaggio da inviare\",\n        \"pair-devices-qr-code_title\": \"Clicca per copiare il link di associazione a questo dispositivo\",\n        \"public-room-qr-code_title\": \"Clicca per copirare il link della stanza pubblica\",\n        \"message_placeholder\": \"Testo\",\n        \"paired-device-removed\": \"Il dispositivo associato è stato rimosso.\",\n        \"base64-title-files\": \"Condividi Files\",\n        \"base64-title-text\": \"Condividi Testo\",\n        \"share-text-subtitle\": \"Modifica messaggio prima dell'invio:\",\n        \"share-text-checkbox\": \"Mostra sempre questa casella di dialogo quando si condivide del testo\",\n        \"approve\": \"accetta\",\n        \"share-text-title\": \"Condividi Messaggio di Testo\",\n        \"close-toast_title\": \"Chiudi notifica\"\n    },\n    \"notifications\": {\n        \"request-title\": \"{{name}} vorrebbe inviare {{count}} {{descriptor}}\",\n        \"unfinished-transfers-warning\": \"Ci sono dei trasferimenti in corso. Sei sicuro di voler chiudere PairDrop?\",\n        \"message-received\": \"Messaggio ricevuto da {{name}} - Clicca per copiare\",\n        \"rate-limit-join-key\": \"Limite raggiunto. Aspetta 10 secondi e riprova.\",\n        \"connecting\": \"Connessione…\",\n        \"pairing-key-invalidated\": \"Il codice {{key}} è stato invalidato\",\n        \"pairing-key-invalid\": \"Codice non valido\",\n        \"connected\": \"Connesso\",\n        \"pairing-not-persistent\": \"I dispositivi associati non sono permanenti\",\n        \"text-content-incorrect\": \"Il contenuto di testo è errato\",\n        \"message-transfer-completed\": \"Trasferimento del messaggio completato\",\n        \"file-transfer-completed\": \"Trasferimento file completato\",\n        \"file-content-incorrect\": \"Il contenuto del file è errato\",\n        \"files-incorrect\": \"I file sono errati\",\n        \"selected-peer-left\": \"Il peer selezionato ha abbandonato\",\n        \"link-received\": \"Link ricevuto da {{name}} - Clicca per aprire\",\n        \"online\": \"Sei di nuovo online\",\n        \"public-room-left\": \"Hai abbandonato la stanza pubblica {{publicRoomId}}\",\n        \"copied-text\": \"Testo copiato negli appunti\",\n        \"display-name-random-again\": \"Il nome visualizzato viene di nuovo generato casualmente\",\n        \"display-name-changed-permanently\": \"Il nome visualizzato è cambiato definitivamente\",\n        \"copied-to-clipboard-error\": \"La funzione di copia non è possibile. Copia manualmente.\",\n        \"pairing-success\": \"Dispositivi associati\",\n        \"clipboard-content-incorrect\": \"Il contenuto copiato è errato\",\n        \"display-name-changed-temporarily\": \"Il nome visualizzato è cambiato solo per questa sessione\",\n        \"copied-to-clipboard\": \"Copiato negli appunti\",\n        \"offline\": \"Sei offline\",\n        \"pairing-tabs-error\": \"Abbinare due schede del browser è impossibile\",\n        \"public-room-id-invalid\": \"ID stanza non valido\",\n        \"click-to-download\": \"Clicca per scaricare\",\n        \"pairing-cleared\": \"Tutti i dispositivi sono stati dissociati\",\n        \"notifications-enabled\": \"Notifiche attivate\",\n        \"online-requirement-pairing\": \"Devi essere online per associare dispositivi\",\n        \"ios-memory-limit\": \"L'invio di file a dispositivi iOS è possibile solo 200 MB alla volta\",\n        \"online-requirement-public-room\": \"Devi essere online per creare una stanza pubblica\",\n        \"copied-text-error\": \"Scrittura negli appunti fallita. Copia manualmente!\",\n        \"download-successful\": \"{{descriptor}} scaricato\",\n        \"click-to-show\": \"Clicca per mostrare\",\n        \"notifications-permissions-error\": \"Il permesso all'invio delle notifiche è stato negato poiché l'utente ha ignorato varie volte le richieste di permesso. Ciò può essere ripristinato nelle \\\"informazioni sito\\\" cliccando sull'icona a forma di lucchetto vicino alla barra degli indirizzi.\",\n        \"pair-url-copied-to-clipboard\": \"Link di associazione copiato negli appunti\",\n        \"room-url-copied-to-clipboard\": \"Link della stanza copiato negli appunti\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"Elaborazione…\",\n        \"click-to-send-share-mode\": \"Clicca per inviare {{descriptor}}\",\n        \"click-to-send\": \"Clicca per inviare files o tasto destro per inviare un messaggio\",\n        \"waiting\": \"In attesa…\",\n        \"connection-hash\": \"Per verificare la sicurezza della crittografia end-to-end, confronta questo numero di sicurezza su entrambi i dispositivi\",\n        \"preparing\": \"Preparazione…\",\n        \"transferring\": \"Trasferimento…\"\n    },\n    \"about\": {\n        \"claim\": \"Il modo più semplice per trasferire files tra dispositivi\",\n        \"tweet_title\": \"Twitta riguardo PairDrop\",\n        \"close-about_aria-label\": \"Chiudi Informazioni su PairDrop\",\n        \"buy-me-a-coffee_title\": \"Offrimi un caffè!\",\n        \"github_title\": \"PairDrop su GitHub\",\n        \"faq_title\": \"Domande Frequenti\",\n        \"mastodon_title\": \"Scrivi su Mastodon di PairDrop\",\n        \"bluesky_title\": \"Seguici su BlueSky\",\n        \"custom_title\": \"Seguici\",\n        \"privacypolicy_title\": \"Apri la nostra policy sulla privacy\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"Trasferimento File Richiesto\",\n        \"image-transfer-requested\": \"Trasferimento Immagine Richiesto\",\n        \"message-received-plural\": \"{{count}} Messaggi Ricevuti\",\n        \"message-received\": \"Messaggio ricevuto\",\n        \"file-received\": \"File Ricevuto\",\n        \"file-received-plural\": \"{{count}} Files Ricevuti\"\n    }\n}\n"
  },
  {
    "path": "public/lang/ja.json",
    "content": "{\n    \"footer\": {\n        \"webrtc\": \"(WebRTCが無効なため)\",\n        \"public-room-devices_title\": \"公開ルーム内のデバイスは、別のネットワークからもアクセスできます。\",\n        \"display-name_data-placeholder\": \"読み込み中…\",\n        \"display-name_title\": \"デバイス名を変更する\",\n        \"traffic\": \"この通信は\",\n        \"paired-devices_title\": \"ペアリング済みデバイスは、別のネットワークからもアクセスできます。\",\n        \"public-room-devices\": \"ルーム{{roomId}}\",\n        \"paired-devices\": \"ペアリング済みデバイス\",\n        \"on-this-network\": \"このネットワーク内\",\n        \"routed\": \"サーバーを経由します\",\n        \"discovery\": \"このデバイスを検出可能なネットワーク:\",\n        \"on-this-network_title\": \"このネットワーク内のすべてのデバイスからアクセスできます。\",\n        \"known-as\": \"このデバイスの名前:\"\n    },\n    \"notifications\": {\n        \"request-title\": \"{{name}}は{{count}}個の{{descriptor}}を共有しようとしています\",\n        \"unfinished-transfers-warning\": \"未完了の転送があります。本当にPairDropを終了しますか？\",\n        \"message-received\": \"{{name}}から受信したメッセージ(クリックしてコピー)\",\n        \"rate-limit-join-key\": \"レート制限に到達しました。10秒待ってから再度お試しください。\",\n        \"connecting\": \"接続中…\",\n        \"pairing-key-invalidated\": \"コード{{key}}は無効になりました\",\n        \"pairing-key-invalid\": \"無効なコード\",\n        \"connected\": \"接続済み\",\n        \"pairing-not-persistent\": \"このデバイスとのペアリングは解除される可能性があります\",\n        \"text-content-incorrect\": \"無効なテキスト内容です\",\n        \"message-transfer-completed\": \"メッセージを送信しました\",\n        \"file-transfer-completed\": \"ファイル転送が完了しました\",\n        \"file-content-incorrect\": \"無効なファイル内容です\",\n        \"files-incorrect\": \"ファイルが間違っています\",\n        \"selected-peer-left\": \"選択したデバイスが退出しました\",\n        \"link-received\": \"{{name}}から受信したリンク(クリックして開く)\",\n        \"online\": \"オンラインに復帰しました\",\n        \"public-room-left\": \"公開ルーム{{publicRoomId}}から退出しました\",\n        \"copied-text\": \"テキストをクリップボードにコピーしました\",\n        \"display-name-random-again\": \"新しいデバイス名に変更されました\",\n        \"display-name-changed-permanently\": \"デバイス名が変更されました\",\n        \"copied-to-clipboard-error\": \"コピーできませんでした。手動でコピーしてください。\",\n        \"pairing-success\": \"ペアリングしました\",\n        \"clipboard-content-incorrect\": \"無効なクリップボード内容です\",\n        \"display-name-changed-temporarily\": \"この接続でのみデバイス名が変更されました\",\n        \"copied-to-clipboard\": \"クリップボードにコピーしました\",\n        \"offline\": \"オフラインです\",\n        \"pairing-tabs-error\": \"同じWebブラウザーで開いたタブ同士でペアリングすることはできません\",\n        \"public-room-id-invalid\": \"無効なルームID\",\n        \"click-to-download\": \"クリックしてダウンロード\",\n        \"pairing-cleared\": \"全てのデバイスとのペアリングを解除しました\",\n        \"notifications-enabled\": \"通知が有効になりました\",\n        \"online-requirement-pairing\": \"ペアリングするにはオンラインである必要があります\",\n        \"ios-memory-limit\": \"iOSへのファイル送信は一度に200MBまでしかできません\",\n        \"online-requirement-public-room\": \"公開ルームを作成するにはオンラインである必要があります\",\n        \"copied-text-error\": \"クリップボードにコピーできませんでした。手動でコピーしてください。\",\n        \"download-successful\": \"{{descriptor}}をダウンロードしました\",\n        \"click-to-show\": \"クリックして表示\",\n        \"notifications-permissions-error\": \"通知がブロックされました。これはユーザーが通知許可を何度も拒否したためです。これをリセットするには、URLバーの隣にある鍵アイコンをクリックして、ページ情報にアクセスしてください。\",\n        \"pair-url-copied-to-clipboard\": \"このデバイスとペアリングするリンクをクリップボードにコピーしました\",\n        \"room-url-copied-to-clipboard\": \"公開ルームへのリンクをクリップボードにコピーしました\"\n    },\n    \"header\": {\n        \"cancel-share-mode\": \"キャンセル\",\n        \"theme-auto_title\": \"システムのテーマに合わせる\",\n        \"install_title\": \"PairDropをインストール\",\n        \"theme-dark_title\": \"常にダークテーマを使用する\",\n        \"pair-device_title\": \"他のデバイスとペアリングする\",\n        \"join-public-room_title\": \"公開ルームに参加する\",\n        \"notification_title\": \"通知を有効にする\",\n        \"edit-paired-devices_title\": \"ペアリング設定\",\n        \"language-selector_title\": \"言語設定\",\n        \"about_title\": \"PairDropについて\",\n        \"about_aria-label\": \"PairDropについてを開く\",\n        \"theme-light_title\": \"常にライトテーマを使用する\",\n        \"edit-share-mode\": \"編集\",\n        \"expand_title\": \"ヘッダーボタン列を拡大する\"\n    },\n    \"instructions\": {\n        \"x-instructions_mobile\": \"タップでファイル送信、長押しでメッセージ送信\",\n        \"x-instructions-share-mode_desktop\": \"クリックして{{descriptor}}を送信\",\n        \"activate-share-mode-and-other-files-plural\": \"とその他{{count}}個のファイル\",\n        \"x-instructions-share-mode_mobile\": \"タップして{{descriptor}}を送信\",\n        \"activate-share-mode-base\": \"他のデバイスでPairDropを開いて送信します\",\n        \"no-peers-subtitle\": \"ペアリングや公開ルームを使うと、別のネットワークにあるデバイスと共有できます\",\n        \"activate-share-mode-shared-text\": \"共有されたテキスト\",\n        \"x-instructions_desktop\": \"左クリックでファイル送信、右クリックでメッセージ送信\",\n        \"no-peers-title\": \"ファイル共有するには他のデバイスでPairDropを開きます\",\n        \"x-instructions_data-drop-peer\": \"ドロップするとこのデバイスに送信します\",\n        \"x-instructions_data-drop-bg\": \"送信したいデバイスの上でドロップしてください\",\n        \"no-peers_data-drop-bg\": \"送信したいデバイスの上でドロップしてください\",\n        \"activate-share-mode-and-other-file\": \"とその他1個のファイル\",\n        \"activate-share-mode-shared-file\": \"共有されたファイル\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}}個の共有されたファイル\",\n        \"webrtc-requirement\": \"このPairDropインスタンスを使用するには、WebRTCを有効にする必要があります！\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"処理中…\",\n        \"click-to-send-share-mode\": \"クリックして{{descriptor}}を送信\",\n        \"click-to-send\": \"左クリックでファイル送信、右クリックでメッセージ送信\",\n        \"waiting\": \"待機中…\",\n        \"connection-hash\": \"エンドツーエンド暗号化のセキュリティを確認するには、両方のデバイスのセキュリティナンバーを確認してください\",\n        \"preparing\": \"準備中…\",\n        \"transferring\": \"転送中…\"\n    },\n    \"dialogs\": {\n        \"base64-paste-to-send\": \"ここをタップして{{type}}を送信\",\n        \"auto-accept-instructions-2\": \"」が有効なら、そのデバイスが送信したすべてのファイルを自動で受け入れます。\",\n        \"receive-text-title\": \"メッセージを受信\",\n        \"edit-paired-devices-title\": \"ペアリング設定\",\n        \"cancel\": \"キャンセル\",\n        \"auto-accept-instructions-1\": \"「\",\n        \"pair-devices-title\": \"新規ペアリング\",\n        \"download\": \"ダウンロード\",\n        \"title-file\": \"ファイル\",\n        \"base64-processing\": \"処理中…\",\n        \"decline\": \"拒否\",\n        \"receive-title\": \"{{descriptor}}を受信しました\",\n        \"leave\": \"退出\",\n        \"join\": \"参加\",\n        \"title-image-plural\": \"複数の画像\",\n        \"send\": \"送信\",\n        \"base64-tap-to-paste\": \"ここをタップして{{type}}を共有\",\n        \"base64-text\": \"テキスト\",\n        \"copy\": \"コピー\",\n        \"file-other-description-image\": \"とその他1個の画像\",\n        \"temporary-public-room-title\": \"公開ルーム\",\n        \"base64-files\": \"ファイル\",\n        \"has-sent\": \"が送信:\",\n        \"file-other-description-file\": \"とその他1個のファイル\",\n        \"close\": \"閉じる\",\n        \"system-language\": \"システムの言語\",\n        \"unpair\": \"ペアリング解除\",\n        \"title-image\": \"画像\",\n        \"file-other-description-file-plural\": \"とその他{{count}}個のファイル\",\n        \"would-like-to-share\": \"がこれを共有しています\",\n        \"send-message-to\": \"このデバイスにメッセージを送信:\",\n        \"language-selector-title\": \"言語設定\",\n        \"pair\": \"ペアリング\",\n        \"hr-or\": \"または\",\n        \"scan-qr-code\": \"QRコードをスキャンしてください。\",\n        \"input-key-on-this-device\": \"このコードを他のデバイスに入力するか\",\n        \"download-again\": \"もう一度ダウンロードする\",\n        \"accept\": \"承諾\",\n        \"paired-devices-wrapper_data-empty\": \"ペアリングしたデバイスはありません。\",\n        \"enter-key-from-another-device\": \"他のデバイスに表示されたコードを入力してください。\",\n        \"share\": \"共有\",\n        \"auto-accept\": \"自動承諾\",\n        \"title-file-plural\": \"複数のファイル\",\n        \"send-message-title\": \"メッセージを送信\",\n        \"input-room-id-on-another-device\": \"このルームIDを他のデバイスに入力するか\",\n        \"file-other-description-image-plural\": \"とその他{{count}}個の画像\",\n        \"enter-room-id-from-another-device\": \"他のデバイスに表示されたルームIDを入力してください。\",\n        \"message_title\": \"送信するメッセージを挿入\",\n        \"pair-devices-qr-code_title\": \"クリックしてこのデバイスとペアリングするリンクをコピー\",\n        \"public-room-qr-code_title\": \"クリックして公開ルームへのリンクをコピー\",\n        \"paired-device-removed\": \"ペアリングを解除しました。\",\n        \"message_placeholder\": \"テキスト\",\n        \"base64-title-files\": \"共有されたファイル\",\n        \"base64-title-text\": \"テキストを共有\",\n        \"approve\": \"OK\",\n        \"share-text-subtitle\": \"送信する前にメッセージを編集する:\",\n        \"share-text-checkbox\": \"テキスト共有する際、毎回この画面を表示する\",\n        \"close-toast_title\": \"通知を閉じる\",\n        \"share-text-title\": \"テキストメッセージを共有します\"\n    },\n    \"about\": {\n        \"claim\": \"デバイス間でかんたんファイル共有\",\n        \"tweet_title\": \"PairDropについてポスト\",\n        \"close-about_aria-label\": \"PairDropについてを閉じる\",\n        \"buy-me-a-coffee_title\": \"コーヒーを一杯おごってください！\",\n        \"github_title\": \"GitHub上のPairDropプロジェクト\",\n        \"faq_title\": \"FAQ\",\n        \"mastodon_title\": \"MastodonでPairDropについてトゥート\",\n        \"bluesky_title\": \"BlueSkyでフォロー\",\n        \"custom_title\": \"フォロー\",\n        \"privacypolicy_title\": \"プライバシーポリシーを開く\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"ファイル転送の要求があります\",\n        \"image-transfer-requested\": \"画像の転送の要求があります\",\n        \"message-received-plural\": \"{{count}}個のメッセージを受信しました\",\n        \"message-received\": \"メッセージを受信しました\",\n        \"file-received\": \"ファイルを受信しました\",\n        \"file-received-plural\": \"{{count}}個のファイルを受信しました\"\n    }\n}\n"
  },
  {
    "path": "public/lang/kab.json",
    "content": "{\n    \"dialogs\": {\n        \"message_placeholder\": \"Aḍris\",\n        \"send-message-title\": \"Azen izen\",\n        \"copy\": \"Nɣel\",\n        \"title-image-plural\": \"Tugniwin\",\n        \"send\": \"Azen\",\n        \"title-image\": \"Tugna\",\n        \"title-file-plural\": \"Ifuyla\",\n        \"base64-text\": \"aḍris\",\n        \"auto-accept-instructions-1\": \"Rmed\",\n        \"base64-files\": \"Ifuyla\",\n        \"hr-or\": \"NEƔ\",\n        \"send-message-to\": \"I:\",\n        \"title-file\": \"Afaylu\",\n        \"close\": \"Mdel\",\n        \"accept\": \"Qbel\",\n        \"share\": \"Beṭṭu\",\n        \"download\": \"Asider\",\n        \"language-selector-title\": \"Sbedd tutlayt\",\n        \"system-language\": \"Tutlayt n unagraw\",\n        \"cancel\": \"Sefsex\"\n    },\n    \"about\": {\n        \"github_title\": \"PairDrop deg GitHub\",\n        \"close-about_aria-label\": \"Mdel Ɣef PairDrop\"\n    },\n    \"notifications\": {\n        \"connecting\": \"Tuqqna…\",\n        \"connected\": \"Yeqqen\"\n    },\n    \"header\": {\n        \"about_aria-label\": \"Ldi Ɣef PairDrop\",\n        \"about_title\": \"Ɣef PairDrop\",\n        \"language-selector_title\": \"Fren tutlayt\",\n        \"install_title\": \"Sbedd PairDrop\",\n        \"edit-share-mode\": \"Ẓreg\"\n    }\n}\n"
  },
  {
    "path": "public/lang/kn.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"PairDrop ಕುರಿತು\",\n        \"cancel-paste-mode\": \"ಮಾಡಿದ\",\n        \"theme-auto_title\": \"ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಸಿಸ್ಟಮ್‌ಗೆ ಥೀಮ್ ಅನ್ನು ಹೊಂದಿಸಿ\",\n        \"install_title\": \"PairDrop ಅನ್ನು ಇನ್ಸ್ಟಾಲ್ ಮಾಡಿ\",\n        \"theme-dark_title\": \"ಯಾವಾಗಲೂ ಡಾರ್ಕ್ ಥೀಮ್ ಅನ್ನು ಬಳಸಿ\",\n        \"pair-device_title\": \"ನಿಮ್ಮ ಸಾಧನಗಳನ್ನು ಶಾಶ್ವತವಾಗಿ ಜೋಡಿ ಮಾಡಿ\",\n        \"join-public-room_title\": \"ತಾತ್ಕಾಲಿಕವಾಗಿ ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಯನ್ನು ಸೇರಿರಿ\",\n        \"notification_title\": \"ಸೂಚನೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ\",\n        \"edit-paired-devices_title\": \"ಜೋಡಿಯಾಗಿರುವ ಸಾಧನಗಳನ್ನು ಎಡಿಟ್ ಮಾಡಿ\",\n        \"language-selector_title\": \"ಭಾಷೆಯನ್ನು ಆಯ್ಕೆ ಮಾಡಿ\",\n        \"about_aria-label\": \"PairDrop ಕುರಿತು ಪುಟವನ್ನು ತೆರೆಯಿರಿ\",\n        \"theme-light_title\": \"ಯಾವಾಗಲೂ ಲೈಟ್ ಥೀಮ್ ಅನ್ನು ಬಳಸಿ\",\n        \"edit-share-mode\": \"ಎಡಿಟ್ ಮಾಡಿ\",\n        \"cancel-share-mode\": \"ರದ್ದುಗೊಳಿಸಿ\",\n        \"expand_title\": \"ಹೆಡರ್ ಬಟನ್ ಸಾಲನ್ನು ವಿಸ್ತರಿಸಿ\"\n    },\n    \"dialogs\": {\n        \"message_placeholder\": \"ಪಠ್ಯ\",\n        \"base64-paste-to-send\": \"{{type}} ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ ಅನ್ನು ಇಲ್ಲಿ ಅಂಟಿಸಿ\",\n        \"auto-accept-instructions-2\": \"ಆ ಸಾಧನದಿಂದ ಕಳುಹಿಸಲಾದ ಎಲ್ಲಾ ಫೈಲ್‌ಗಳನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಸ್ವೀಕರಿಸಲು.\",\n        \"receive-text-title\": \"ಸಂದೇಶವನ್ನು ಸ್ವೀಕರಿಸಲಾಗಿದೆ\",\n        \"edit-paired-devices-title\": \"ಜೋಡಿಯಾಗಿರುವ ಸಾಧನಗಳನ್ನು ಎಡಿಟ್ ಮಾಡಿ\",\n        \"cancel\": \"ರದ್ದುಗೊಳಿಸಿ\",\n        \"auto-accept-instructions-1\": \"ಸಕ್ರಿಯಗೊಳಿಸಿ\",\n        \"pair-devices-title\": \"ಸಾಧನಗಳನ್ನು ಶಾಶ್ವತವಾಗಿ ಜೋಡಿಸಿ\",\n        \"download\": \"ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ\",\n        \"title-file\": \"ಫೈಲ್\",\n        \"base64-processing\": \"ಪ್ರಕ್ರಿಯೆಗೊಳಿಸಲಾಗುತ್ತಿದೆ…\",\n        \"decline\": \"ನಿರಾಕರಿಸಿ\",\n        \"receive-title\": \"{{descriptor}} ಸ್ವೀಕರಿಸಲಾಗಿದೆ\",\n        \"leave\": \"ಬಿಡಿ\",\n        \"message_title\": \"ಕಳುಹಿಸಲು ಸಂದೇಶವನ್ನು ನಮೂದಿಸಿ\",\n        \"join\": \"ಸೇರಿಕೊಳ್ಳಿ\",\n        \"title-image-plural\": \"ಚಿತ್ರಗಳು\",\n        \"send\": \"ಕಳುಹಿಸಿ\",\n        \"base64-tap-to-paste\": \"{{type}} ಹಂಚಿಕೊಳ್ಳಲು ಇಲ್ಲಿ ಟ್ಯಾಪ್ ಮಾಡಿ\",\n        \"base64-text\": \"ಪಠ್ಯ\",\n        \"copy\": \"ನಕಲು ಮಾಡಿ\",\n        \"file-other-description-image\": \"ಮತ್ತು ಇನ್ನೊಂದು ಚಿತ್ರ\",\n        \"pair-devices-qr-code_title\": \"ಈ ಸಾಧನವನ್ನು ಜೋಡಿಸಲು ಬಳಸುವ ಲಿಂಕ್ ನಕಲಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"temporary-public-room-title\": \"ತಾತ್ಕಾಲಿಕ ಸಾರ್ವಜನಿಕ ಕೊಠಡಿ\",\n        \"base64-files\": \"ಫೈಲ್‌ಗಳು\",\n        \"has-sent\": \"ಕಳುಹಿಸಿದ್ದಾರೆ:\",\n        \"file-other-description-file\": \"ಮತ್ತು ಇನ್ನೊಂದು ಫೈಲ್\",\n        \"public-room-qr-code_title\": \"ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಗೆ ಲಿಂಕ್ ಅನ್ನು ನಕಲಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"close\": \"ಮುಚ್ಚಿ\",\n        \"system-language\": \"ಸಿಸ್ಟಮ್ ಭಾಷೆ\",\n        \"unpair\": \"ಜೋಡಿಯನ್ನು ತೆಗೆಯಿರಿ\",\n        \"title-image\": \"ಚಿತ್ರ\",\n        \"file-other-description-file-plural\": \"ಮತ್ತು {{count}} ಇತರ ಫೈಲ್‌ಗಳು\",\n        \"would-like-to-share\": \"ಹಂಚಿಕೊಳ್ಳಲು ಬಯಸುತ್ತಾರೆ\",\n        \"send-message-to\": \"ಇವರಿಗೆ:\",\n        \"language-selector-title\": \"ಭಾಷೆಯನ್ನು ಹೊಂದಿಸಿ\",\n        \"pair\": \"ಜೋಡಿ\",\n        \"hr-or\": \"ಅಥವಾ\",\n        \"scan-qr-code\": \"ಅಥವಾ QR-ಕೋಡ್ ಅನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿ.\",\n        \"input-key-on-this-device\": \"ಇನ್ನೊಂದು ಸಾಧನದಲ್ಲಿ ಈ ಕೀಲಿಯನ್ನು ನಮೂದಿಸಿ\",\n        \"download-again\": \"ಮತ್ತೊಮ್ಮೆ ಡೌನ್ಲೋಡ್ ಮಾಡಿ\",\n        \"accept\": \"ಒಪ್ಪಿಕೊಳ್ಳಿ\",\n        \"paired-devices-wrapper_data-empty\": \"ಜೋಡಿಯಾಗಿರುವ ಸಾಧನಗಳಿಲ್ಲ.\",\n        \"enter-key-from-another-device\": \"ಇನ್ನೊಂದು ಸಾಧನದಿಂದ ಕೀಲಿಯನ್ನು ಇಲ್ಲಿ ನಮೂದಿಸಿ.\",\n        \"share\": \"ಹಂಚಿಕೊಳ್ಳಿ\",\n        \"auto-accept\": \"ಸ್ವಯಂ ಸ್ವೀಕರಿಸು\",\n        \"title-file-plural\": \"ಫೈಲ್‌ಗಳು\",\n        \"send-message-title\": \"ಸಂದೇಶ ಕಳುಹಿಸಿ\",\n        \"input-room-id-on-another-device\": \"ಇನ್ನೊಂದು ಸಾಧನದಲ್ಲಿ ಈ ರೂಮ್ ಐಡಿಯನ್ನು ನಮೂದಿಸಿ\",\n        \"file-other-description-image-plural\": \"ಮತ್ತು {{count}} ಇತರ ಚಿತ್ರಗಳು\",\n        \"enter-room-id-from-another-device\": \"ಕೊಠಡಿ ಸೇರಲು ಇನ್ನೊಂದು ಸಾಧನದಿಂದ ರೂಮ್ ಐಡಿ ನಮೂದಿಸಿ.\",\n        \"close-toast_title\": \"ಅಧಿಸೂಚನೆಯನ್ನು ಮುಚ್ಚಿರಿ\",\n        \"share-text-checkbox\": \"ಪಠ್ಯವನ್ನು ಹಂಚಿಕೊಳ್ಳುವಾಗ ಯಾವಾಗಲೂ ಈ ಡೈಲಾಗ್ ಅನ್ನು ತೋರಿಸಿ\",\n        \"base64-title-files\": \"ಫೈಲ್‌ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಿ\",\n        \"approve\": \"ಅನುಮೋದಿಸಿ\",\n        \"paired-device-removed\": \"ಜೋಡಿಸಲಾದ ಸಾಧನವನ್ನು ತೆಗೆದುಹಾಕಲಾಗಿದೆ.\",\n        \"share-text-title\": \"ಪಠ್ಯ ಸಂದೇಶವನ್ನು ಹಂಚಿಕೊಳ್ಳಿ\",\n        \"share-text-subtitle\": \"ಸಂದೇಶವನ್ನು ಕಳುಹಿಸುವ ಮೊದಲು ಎಡಿಟ್ ಮಾಡಿ:\",\n        \"base64-title-text\": \"ಪಠ್ಯವನ್ನು ಹಂಚಿಕೊಳ್ಳಿ\"\n    },\n    \"footer\": {\n        \"webrtc\": \"WebRTC ಲಭ್ಯವಿಲ್ಲದಿದ್ದರೆ.\",\n        \"public-room-devices_title\": \"ನೆಟ್‌ವರ್ಕ್‌ನಿಂದ ಸ್ವತಂತ್ರವಾದ ಈ ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಯಲ್ಲಿನ ಸಾಧನಗಳ ಮೂಲಕ ನಿಮ್ಮನ್ನು ಕಂಡುಹಿಡಿಯಬಹುದು.\",\n        \"display-name_data-placeholder\": \"ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ…\",\n        \"display-name_title\": \"ನಿಮ್ಮ ಸಾಧನದ ಹೆಸರನ್ನು ಶಾಶ್ವತವಾಗಿ ಎಡಿಟ್ ಮಾಡಿ\",\n        \"traffic\": \"ಟ್ರಾಫಿಕ್ ಅನ್ನು\",\n        \"paired-devices_title\": \"ನೆಟ್‌ವರ್ಕ್‌ನಿಂದ ಸ್ವತಂತ್ರವಾಗಿ ಎಲ್ಲಾ ಸಮಯದಲ್ಲೂ ಜೋಡಿಸಲಾದ ಸಾಧನಗಳಿಂದ ನಿಮ್ಮನ್ನು ಕಂಡುಹಿಡಿಯಬಹುದು.\",\n        \"public-room-devices\": \"{{roomId}} ಕೊಠಡಿಯಲ್ಲಿ\",\n        \"paired-devices\": \"ಜೋಡಿಸಲಾದ ಸಾಧನಗಳ ಮೂಲಕ\",\n        \"on-this-network\": \"ಈ ನೆಟ್ವರ್ಕ್ನಲ್ಲಿ\",\n        \"routed\": \"ಸರ್ವರ್ ಮೂಲಕ ರವಾನಿಸಲಾಗಿದೆ\",\n        \"discovery\": \"ನಿಮ್ಮನ್ನು ಕಂಡುಹಿಡಿಯಲಾಗುವುದು:\",\n        \"on-this-network_title\": \"ಈ ನೆಟ್‌ವರ್ಕ್‌ನಲ್ಲಿರುವ ಪ್ರತಿಯೊಬ್ಬರಿಂದ ನಿಮ್ಮನ್ನು ಕಂಡುಹಿಡಿಯಬಹುದು.\",\n        \"known-as\": \"ನಿಮ್ಮನ್ನು ಹೀಗೆ ಕರೆಯಲಾಗುತ್ತದೆ:\"\n    },\n    \"notifications\": {\n        \"request-title\": \"{{name}} ಅವರು {{count}} {{descriptor}} ಅನ್ನು ವರ್ಗಾಯಿಸಲು ಬಯಸುತ್ತಾರೆ\",\n        \"unfinished-transfers-warning\": \"ಅಪೂರ್ಣ ವರ್ಗಾವಣೆಗಳಿವೆ. PairDrop ಅನ್ನು ಮುಚ್ಚಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ?\",\n        \"message-received\": \"{{name}} ಅವರಿಂದ ಸಂದೇಶವನ್ನು ಸ್ವೀಕರಿಸಲಾಗಿದೆ - ನಕಲಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"notifications-permissions-error\": \"ಬಳಕೆದಾರರು ಹಲವಾರು ಬಾರಿ ಅನುಮತಿ ಪ್ರಾಂಪ್ಟ್ ಅನ್ನು ವಜಾಗೊಳಿಸಿರುವುದರಿಂದ ಸೂಚನೆಗಳ ಅನುಮತಿಯನ್ನು ನಿರ್ಬಂಧಿಸಲಾಗಿದೆ. ಇದನ್ನು ಪುಟ ಮಾಹಿತಿಯಲ್ಲಿ ಮರುಹೊಂದಿಸಬಹುದು, URL ಬಾರ್‌ನ ಪಕ್ಕದಲ್ಲಿರುವ ಬೀಗದ ಐಕಾನ್ ಕ್ಲಿಕ್ ಮಾಡುವ ಮೂಲಕ ಪ್ರವೇಶಿಸಬಹುದು.\",\n        \"rate-limit-join-key\": \"ದರದ ಮಿತಿ ತಲುಪಿದೆ. ೧೦ ಸೆಕೆಂಡುಗಳು ನಿರೀಕ್ಷಿಸಿ ಮತ್ತು ಪುನಃ ಪ್ರಯತ್ನಿಸಿ.\",\n        \"pair-url-copied-to-clipboard\": \"ಈ ಸಾಧನವನ್ನು ಜೋಡಿಸಲು ಬಳಸುವ ಲಿಂಕ್ ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಲಾಗಿದೆ\",\n        \"connecting\": \"ಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ…\",\n        \"pairing-key-invalidated\": \"ಕೀಲಿ {{key}} ಅಮಾನ್ಯಗೊಂಡಿದೆ\",\n        \"pairing-key-invalid\": \"ಅಮಾನ್ಯವಾದ ಕೀಲಿ\",\n        \"connected\": \"ಸಂಪರ್ಕಿಸಲಾಗಿದೆ\",\n        \"pairing-not-persistent\": \"ಜೋಡಿಯಾಗಿರುವ ಸಾಧನಗಳು ನಿರಂತರವಾಗಿರುವುದಿಲ್ಲ\",\n        \"text-content-incorrect\": \"ಪಠ್ಯದ ವಿಷಯ ತಪ್ಪಾಗಿದೆ\",\n        \"message-transfer-completed\": \"ಸಂದೇಶ ವರ್ಗಾವಣೆ ಪೂರ್ಣಗೊಂಡಿದೆ\",\n        \"file-transfer-completed\": \"ಫೈಲ್ ವರ್ಗಾವಣೆ ಮುಗಿದಿದೆ\",\n        \"file-content-incorrect\": \"ಫೈಲ್ ವಿಷಯವು ತಪ್ಪಾಗಿದೆ\",\n        \"files-incorrect\": \"ಫೈಲ್‌ಗಳು ಸರಿಯಾಗಿಲ್ಲ\",\n        \"selected-peer-left\": \"ಆಯ್ದ ಪೀರ್ ತೊರೆದಿದ್ದಾರೆ\",\n        \"link-received\": \"{{name}} ಮೂಲಕ ಲಿಂಕ್ ಸ್ವೀಕರಿಸಲಾಗಿದೆ - ತೆರೆಯಲು ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"online\": \"ನೀವು ಆನ್‌ಲೈನ್‌ ಮರಳಿದಿರಿ\",\n        \"public-room-left\": \"ಸಾರ್ವಜನಿಕ ಕೊಠಡಿ {{publicRoomId}} ಯನ್ನು ತೊರೆದಿರುವಿರಿ\",\n        \"copied-text\": \"ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ಪಠ್ಯವನ್ನು ನಕಲಿಸಲಾಗಿದೆ\",\n        \"display-name-random-again\": \"ಪ್ರದರ್ಶನದ ಹೆಸರನ್ನು ಯಾದೃಚ್ಛಿಕವಾಗಿ ಮತ್ತೆ ರಚಿಸಲಾಗಿದೆ\",\n        \"display-name-changed-permanently\": \"ಪ್ರದರ್ಶನದ ಹೆಸರನ್ನು ಶಾಶ್ವತವಾಗಿ ಬದಲಾಯಿಸಲಾಗಿದೆ\",\n        \"copied-to-clipboard-error\": \"ನಕಲು ಮಾಡುವುದು ಅಸಾಧ್ಯ. ಕೈಯಾರೆ ನಕಲಿಸಿ.\",\n        \"pairing-success\": \"ಸಾಧನಗಳನ್ನು ಜೋಡಿಸಲಾಗಿದೆ\",\n        \"clipboard-content-incorrect\": \"ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ನ ವಿಷಯವು ತಪ್ಪಾಗಿದೆ\",\n        \"display-name-changed-temporarily\": \"ಪ್ರದರ್ಶನದ ಹೆಸರನ್ನು ಈ ಸೆಶನ್‌ಗೆ ಮಾತ್ರ ಬದಲಾಯಿಸಲಾಗಿದೆ\",\n        \"copied-to-clipboard\": \"ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಲಾಗಿದೆ\",\n        \"offline\": \"ನೀವು ಆಫ್‌ಲೈನ್‌ ಇದ್ದೀರಿ\",\n        \"pairing-tabs-error\": \"ಎರಡು ವೆಬ್ ಬ್ರೌಸರ್ ಟ್ಯಾಬ್‌ಗಳನ್ನು ಜೋಡಿಸುವುದು ಅಸಾಧ್ಯ\",\n        \"public-room-id-invalid\": \"ಅಮಾನ್ಯವಾದ ಕೊಠಡಿ ಐಡಿ\",\n        \"click-to-download\": \"ಡೌನ್ಲೋಡ್ ಮಾಡಲು ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"pairing-cleared\": \"ಎಲ್ಲಾ ಸಾಧನಗಳನ್ನು ಜೋಡಿಯಾಗಿ ತೆಗೆಯಲಾಗಿದೆ\",\n        \"notifications-enabled\": \"ಸೂಚನೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ\",\n        \"online-requirement-pairing\": \"ಸಾಧನಗಳನ್ನು ಜೋಡಿಸಲು ನೀವು ಆನ್‌ಲೈನ್‌ ಇರಬೇಕು\",\n        \"ios-memory-limit\": \"iOSಗೆ ಫೈಲ್‌ಗಳನ್ನು ಕಳುಹಿಸುವುದು ಒಂದೇ ಬಾರಿಗೆ 200 MB ವರೆಗೆ ಮಾತ್ರ ಸಾಧ್ಯವಾಗಿದೆ\",\n        \"online-requirement-public-room\": \"ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಯನ್ನು ರಚಿಸಲು ನೀವು ಆನ್‌ಲೈನ್‌ ಇರಬೇಕು\",\n        \"room-url-copied-to-clipboard\": \"ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಯ ಲಿಂಕ್ ಅನ್ನು ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಲಾಗಿದೆ\",\n        \"copied-text-error\": \"ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ಬರೆಯುವುದು ವಿಫಲವಾಗಿದೆ. ಕೈಯಾರೆ ನಕಲಿಸಿ!\",\n        \"download-successful\": \"{{descriptor}} ಅನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ\",\n        \"click-to-show\": \"ತೋರಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ\"\n    },\n    \"instructions\": {\n        \"x-instructions_mobile\": \"ಫೈಲ್‌ಗಳನ್ನು ಕಳುಹಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ ಅಥವಾ ಸಂದೇಶವನ್ನು ಕಳುಹಿಸಲು ದೀರ್ಘವಾಗಿ ಟ್ಯಾಪ್ ಮಾಡಿ\",\n        \"click-to-send\": \"ಕಳುಹಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"activate-paste-mode-and-other-files\": \"ಮತ್ತು ಇತರ {{count}} ಫೈಲ್‌ಗಳು\",\n        \"tap-to-send\": \"ಕಳುಹಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ\",\n        \"activate-paste-mode-base\": \"ಕಳುಹಿಸಲು ಇತರ ಸಾಧನಗಳಲ್ಲಿ PairDrop ತೆರೆಯಿರಿ\",\n        \"no-peers-subtitle\": \"ಇತರ ನೆಟ್‌ವರ್ಕ್‌ಗಳಲ್ಲಿ ಅನ್ವೇಷಿಸಲು ಸಾಧನಗಳನ್ನು ಜೋಡಿಸಿ ಅಥವಾ ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಯನ್ನು ನಮೂದಿಸಿ\",\n        \"activate-paste-mode-shared-text\": \"ಹಂಚಿದ ಪಠ್ಯ\",\n        \"x-instructions_desktop\": \"ಫೈಲ್‌ಗಳನ್ನು ಕಳುಹಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ ಅಥವಾ ಸಂದೇಶ ಕಳುಹಿಸಲು ಬಲ ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"no-peers-title\": \"ಫೈಲ್‌ಗಳನ್ನು ಕಳುಹಿಸಲು PairDrop ಅನ್ನು ಇತರ ಸಾಧನಗಳಲ್ಲಿ ತೆರೆಯಿರಿ\",\n        \"x-instructions_data-drop-peer\": \"ಪೀರ್‌ಗೆ ಕಳುಹಿಸಲು ಬಿಡುಗಡೆ ಮಾಡಿ\",\n        \"x-instructions_data-drop-bg\": \"ಸ್ವೀಕರಿಸುವವರನ್ನು ಆಯ್ಕೆ ಮಾಡಲು ಬಿಡುಗಡೆ ಮಾಡಿ\",\n        \"no-peers_data-drop-bg\": \"ಸ್ವೀಕರಿಸುವವರನ್ನು ಆಯ್ಕೆ ಮಾಡಲು ಬಿಡುಗಡೆ ಮಾಡಿ\",\n        \"webrtc-requirement\": \"ಈ PairDrop ನಿದರ್ಶನವನ್ನು ಬಳಸಲು, WebRTC ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬೇಕು!\",\n        \"activate-share-mode-base\": \"ಕಳುಹಿಸಲು ಇತರ ಸಾಧನಗಳಲ್ಲಿ PairDrop ತೆರೆಯಿರಿ\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} ಹಂಚಿದ ಫೈಲ್‌ಗಳು\",\n        \"x-instructions-share-mode_desktop\": \"{{descriptor}} ಕಳುಹಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"activate-share-mode-shared-file\": \"ಹಂಚಿದ ಫೈಲ್\",\n        \"activate-share-mode-and-other-file\": \"ಮತ್ತು ಇತರ 1 ಫೈಲ್\",\n        \"x-instructions-share-mode_mobile\": \"{{descriptor}} ಕಳುಹಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ\",\n        \"activate-share-mode-and-other-files-plural\": \"ಮತ್ತು ಇತರ {{count}} ಫೈಲ್‌ಗಳು\",\n        \"activate-share-mode-shared-text\": \"ಹಂಚಿದ ಪಠ್ಯ\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"ಪ್ರಕ್ರಿಯೆಗೊಳಿಸಲಾಗುತ್ತಿದೆ…\",\n        \"click-to-send-paste-mode\": \"{{descriptor}} ಕಳುಹಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"click-to-send\": \"ಫೈಲ್‌ಗಳನ್ನು ಕಳುಹಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ ಅಥವಾ ಸಂದೇಶ ಕಳುಹಿಸಲು ಬಲ ಕ್ಲಿಕ್ ಮಾಡಿ\",\n        \"waiting\": \"ನಿರೀಕ್ಷಿಸಲಾಗುತ್ತಿದೆ…\",\n        \"connection-hash\": \"ಎಂಡ್-ಟು-ಎಂಡ್ ಎನ್‌ಕ್ರಿಪ್ಶನ್‌ನ ಭದ್ರತೆಯನ್ನು ಪರಿಶೀಲಿಸಲು, ಎರಡೂ ಸಾಧನಗಳಲ್ಲಿ ಈ ಭದ್ರತಾ ಸಂಖ್ಯೆಯನ್ನು ಹೋಲಿಸಿ\",\n        \"preparing\": \"ಸಿದ್ಧಪಡಿಸಲಾಗುತ್ತಿದೆ…\",\n        \"transferring\": \"ವರ್ಗಾಯಿಸಲಾಗುತ್ತಿದೆ…\",\n        \"click-to-send-share-mode\": \"{{descriptor}} ಕಳುಹಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ\"\n    },\n    \"about\": {\n        \"claim\": \"ಸಾಧನಗಳಾದ್ಯಂತ ಫೈಲ್‌ಗಳನ್ನು ವರ್ಗಾಯಿಸಲು ಸುಲಭವಾದ ಮಾರ್ಗ\",\n        \"tweet_title\": \"PairDrop ಕುರಿತು ಟ್ವೀಟ್ ಮಾಡಿ\",\n        \"close-about_aria-label\": \"PairDrop ಕುರಿತು ಪುಟವನ್ನು ಮುಚ್ಚಿ\",\n        \"buy-me-a-coffee_title\": \"ನನಗೆ ಕಾಫಿ ಖರೀದಿಸಿ!\",\n        \"github_title\": \"GitHub ನಲ್ಲಿ PairDrop\",\n        \"faq_title\": \"ಪದೇ ಪದೇ ಕೇಳಲಾಗುವ ಪ್ರಶ್ನೆಗಳು\",\n        \"bluesky_title\": \"BlueSky ನಲ್ಲಿ ನಮ್ಮನ್ನು ಅನುಸರಿಸಿ\",\n        \"privacypolicy_title\": \"ನಮ್ಮ ಗೌಪ್ಯತೆ ನೀತಿ ತೆರೆಯಿರಿ\",\n        \"mastodon_title\": \"Mastodon ನಲ್ಲಿ PairDrop ಕುರಿತು ಬರೆಯಿರಿ\",\n        \"custom_title\": \"ನಮ್ಮನ್ನು ಅನುಸರಿಸಿ\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"ಫೈಲ್ ವರ್ಗಾವಣೆಗೆ ವಿನಂತಿಸಲಾಗಿದೆ\",\n        \"image-transfer-requested\": \"ಚಿತ್ರದ ವರ್ಗಾವಣೆಯನ್ನು ವಿನಂತಿಸಲಾಗಿದೆ\",\n        \"message-received-plural\": \"{{count}} ಸಂದೇಶಗಳನ್ನು ಸ್ವೀಕರಿಸಲಾಗಿದೆ\",\n        \"message-received\": \"ಸಂದೇಶವನ್ನು ಸ್ವೀಕರಿಸಲಾಗಿದೆ\",\n        \"file-received\": \"ಫೈಲ್ ಸ್ವೀಕರಿಸಲಾಗಿದೆ\",\n        \"file-received-plural\": \"{{count}} ಫೈಲ್‌ಗಳನ್ನು ಸ್ವೀಕರಿಸಲಾಗಿದೆ\"\n    }\n}\n"
  },
  {
    "path": "public/lang/ko.json",
    "content": "{\n    \"header\": {\n        \"notification_title\": \"알림 켜기\",\n        \"edit-paired-devices_title\": \"연결된 기기 편집하기\",\n        \"install_title\": \"PairDrop 설치\",\n        \"theme-dark_title\": \"항상 다크 테마 사용\",\n        \"about_title\": \"PairDrop에 대하여\",\n        \"language-selector_title\": \"언어 설정\",\n        \"about_aria-label\": \"PairDrop에 대하여 보기\",\n        \"theme-light_title\": \"항상 라이트 테마 사용\",\n        \"theme-auto_title\": \"시스템에 맞춰 테마 자동 적용\",\n        \"join-public-room_title\": \"일시적으로 공개 방에 참여하기\",\n        \"cancel-share-mode\": \"취소\",\n        \"edit-share-mode\": \"편집\",\n        \"pair-device_title\": \"영구적으로 기기 연결하기\",\n        \"expand_title\": \"헤더 버튼 펼치기\"\n    },\n    \"instructions\": {\n        \"no-peers-subtitle\": \"장치를 연결하거나 공개 방에 들어가 다른 네트워크에서 검색 가능하게 하세요\",\n        \"no-peers_data-drop-bg\": \"해제하여 수신자 선택하기\",\n        \"x-instructions_mobile\": \"탭하여 파일을 보내거나 길게 탭하여 메시지를 보내세요\",\n        \"x-instructions_desktop\": \"클릭해 파일을 보내거나 오른쪽 클릭으로 메시지를 보내세요\",\n        \"no-peers-title\": \"다른 장치에서 PairDrop을 열어 파일 보내기\",\n        \"x-instructions_data-drop-bg\": \"해제하여 수신자 선택하기\",\n        \"x-instructions-share-mode_desktop\": \"클릭하여 {{descriptor}} 보내기\",\n        \"x-instructions-share-mode_mobile\": \"탭하여 {{descriptor}} 보내기\",\n        \"activate-share-mode-base\": \"다른 기기에서 PairDrop을 열어 보내기\",\n        \"activate-share-mode-and-other-files-plural\": \"이외 {{count}}개의 다른 파일\",\n        \"activate-share-mode-and-other-file\": \"이외 1개의 다른 파일\",\n        \"x-instructions_data-drop-peer\": \"놓아서 피어에게 보내기\",\n        \"activate-share-mode-shared-text\": \"공유된 텍스트\",\n        \"activate-share-mode-shared-file\": \"공유된 파일\",\n        \"activate-share-mode-shared-files-plural\": \"공유된 파일 {{count}}개\",\n        \"webrtc-requirement\": \"PairDrop 인스턴스를 사용하려면 WebRTC가 활성화되어야 합니다!\"\n    },\n    \"dialogs\": {\n        \"base64-processing\": \"처리 중…\",\n        \"file-other-description-file\": \"그리고 파일 1개\",\n        \"pair-devices-title\": \"영구적으로 기기 페어링하기\",\n        \"input-key-on-this-device\": \"이 키를 다른 장치에서 입력하거나\",\n        \"scan-qr-code\": \"QR 코드를 스캔하세요.\",\n        \"enter-key-from-another-device\": \"다른 장치의 키를 여기에 입력하세요.\",\n        \"temporary-public-room-title\": \"임시 공개 방\",\n        \"input-room-id-on-another-device\": \"이 방 ID를 다른 기기에서 입력하세요\",\n        \"enter-room-id-from-another-device\": \"방에 참가하려면 다른 장치에 표시된 방 ID를 입력하세요.\",\n        \"hr-or\": \"또는\",\n        \"pair\": \"페어링\",\n        \"cancel\": \"취소\",\n        \"edit-paired-devices-title\": \"페어링된 장치 관리\",\n        \"unpair\": \"페어링 해제\",\n        \"paired-device-removed\": \"페어링된 장치가 제거되었습니다.\",\n        \"paired-devices-wrapper_data-empty\": \"페어링된 장치가 없습니다.\",\n        \"auto-accept\": \"자동 수신\",\n        \"auto-accept-instructions-1\": \"\",\n        \"auto-accept-instructions-2\": \"을 활성화하여 이 장치에서 보내는 모든 파일을 자동으로 수신하세요.\",\n        \"close\": \"닫기\",\n        \"join\": \"참가\",\n        \"leave\": \"퇴장\",\n        \"would-like-to-share\": \"님이 아래 내용을 공유하고 싶어합니다\",\n        \"accept\": \"수락\",\n        \"decline\": \"거절\",\n        \"has-sent\": \"님이 보냄:\",\n        \"share\": \"공유\",\n        \"download\": \"다운로드\",\n        \"send-message-title\": \"메시지 전송\",\n        \"send-message-to\": \"받는 사람:\",\n        \"message_title\": \"보낼 메시지를 입력하세요\",\n        \"message_placeholder\": \"텍스트\",\n        \"send\": \"전송\",\n        \"receive-text-title\": \"메시지 받음\",\n        \"copy\": \"복사\",\n        \"base64-title-files\": \"파일 공유\",\n        \"base64-title-text\": \"텍스트 공유\",\n        \"base64-tap-to-paste\": \"여기를 눌러 {{type}}을(를) 공유하세요\",\n        \"base64-paste-to-send\": \"여기에 클립보드에서 붙여넣어 {{type}}을(를) 공유하세요\",\n        \"base64-text\": \"텍스트\",\n        \"base64-files\": \"파일\",\n        \"file-other-description-image\": \"그리고 이미지 1개\",\n        \"file-other-description-image-plural\": \"그리고 이미지 {{count}}개\",\n        \"file-other-description-file-plural\": \"그리고 파일 {{count}}개\",\n        \"title-image\": \"이미지\",\n        \"title-file\": \"파일\",\n        \"title-image-plural\": \"이미지\",\n        \"title-file-plural\": \"파일\",\n        \"receive-title\": \"{{descriptor}} 받음\",\n        \"download-again\": \"다시 다운로드\",\n        \"language-selector-title\": \"언어 설정\",\n        \"system-language\": \"시스템 언어\",\n        \"public-room-qr-code_title\": \"클릭하여 공개 방 링크를 복사하세요\",\n        \"pair-devices-qr-code_title\": \"클릭하여 이 장치와 페어링할 수 있는 링크를 복사하세요\",\n        \"share-text-title\": \"텍스트 메시지 공유\",\n        \"approve\": \"확인\",\n        \"share-text-subtitle\": \"보내기 전에 메시지 수정:\",\n        \"share-text-checkbox\": \"텍스트를 공유하기 전에 항상 이 창 띄우기\",\n        \"close-toast_title\": \"알림 닫기\"\n    },\n    \"notifications\": {\n        \"pairing-tabs-error\": \"브라우저 탭 2개를 페어링할 수 없습니다\",\n        \"display-name-changed-permanently\": \"표시 이름이 영구적으로 변경되었습니다\",\n        \"display-name-changed-temporarily\": \"표시 이름이 이 세션에 대하여 변경되었습니다\",\n        \"display-name-random-again\": \"표시 이름이 다시 무작위로 생성됩니다\",\n        \"download-successful\": \"{{descriptor}}을(를) 다운로드했습니다\",\n        \"pairing-success\": \"장치가 페어링되었습니다\",\n        \"pairing-not-persistent\": \"이 장치와의 페어링이 해제될 수 있습니다\",\n        \"pairing-key-invalid\": \"유효하지 않은 키입니다\",\n        \"pairing-key-invalidated\": \"키 {{key}} 만료됨\",\n        \"pairing-cleared\": \"모든 장치와의 페어링을 해제했습니다\",\n        \"public-room-id-invalid\": \"유효하지 않은 방 ID입니다\",\n        \"public-room-left\": \"공개 방 {{publicRoomId}}에서 퇴장했습니다\",\n        \"copied-to-clipboard\": \"클립보드에 복사했습니다\",\n        \"pair-url-copied-to-clipboard\": \"이 장치와 페어링할 수 있는 링크를 복사했습니다\",\n        \"copied-to-clipboard-error\": \"복사하지 못했습니다. 직접 복사하세요.\",\n        \"text-content-incorrect\": \"텍스트가 올바르지 않습니다\",\n        \"file-content-incorrect\": \"파일이 올바르지 않습니다\",\n        \"clipboard-content-incorrect\": \"클립보드 내용이 올바르지 않습니다\",\n        \"room-url-copied-to-clipboard\": \"공개 방 링크를 클립보드에 복사했습니다\",\n        \"notifications-permissions-error\": \"권한 요청을 거부하여 알림을 사용할 수 없습니다. 주소 바 옆 자물쇠 아이콘의 페이지 설정에서 초기화할 수 있습니다.\",\n        \"notifications-enabled\": \"알림을 켰습니다\",\n        \"link-received\": \"{{name}} 님이 보낸 링크 - 클릭하여 열어 보세요\",\n        \"message-received\": \"{{name}} 님이 보낸 메시지 - 클릭하여 복사하세요\",\n        \"click-to-download\": \"클릭하여 다운로드하세요\",\n        \"request-title\": \"{{name}} 님이 {{count}}개의 {{descriptor}}을(를) 보내고 싶어합니다\",\n        \"click-to-show\": \"클릭하여 표시하세요\",\n        \"copied-text\": \"텍스트를 클립보드에 복사했습니다\",\n        \"copied-text-error\": \"클립보드에 복사하지 못했습니다. 직접 복사하세요!\",\n        \"offline\": \"오프라인 상태입니다\",\n        \"online\": \"온라인이 되었습니다\",\n        \"connected\": \"연결되었습니다\",\n        \"online-requirement-pairing\": \"장치와 페어링하려면 온라인 상태여야 합니다\",\n        \"online-requirement-public-room\": \"공개 방을 만드려면 온라인 상태여야 합니다\",\n        \"connecting\": \"연결 중…\",\n        \"files-incorrect\": \"파일이 올바르지 않습니다\",\n        \"file-transfer-completed\": \"파일 전송 완료\",\n        \"ios-memory-limit\": \"iOS로는한 번에 최대 200 MB까지만 전송할 수 있습니다\",\n        \"message-transfer-completed\": \"메시지 전송 완료\",\n        \"unfinished-transfers-warning\": \"끝나지 않은 전송이 있습니다. 정말로 PairDrop을 닫으시겠습니까?\",\n        \"rate-limit-join-key\": \"횟수 제한에 도달했습니다. 10초 후 다시 시도해 주세요.\",\n        \"selected-peer-left\": \"선택한 장치가 퇴장하였습니다\"\n    },\n    \"footer\": {\n        \"known-as\": \"당신의 이름:\",\n        \"display-name_data-placeholder\": \"불러오는 중…\",\n        \"display-name_title\": \"기기 이름 영구적으로 설정하기\",\n        \"discovery\": \"아래에서 발견 가능:\",\n        \"on-this-network\": \"이 네트워크\",\n        \"on-this-network_title\": \"이 네트워크에 있는 모든 사람에게 발견될 수 있습니다.\",\n        \"paired-devices\": \"페어링된 기기\",\n        \"paired-devices_title\": \"네트워크에 관계 없이 항상 페어링된 기기에서 발견될 수 있습니다.\",\n        \"public-room-devices\": \"방 {{roomId}}\",\n        \"public-room-devices_title\": \"네트워크에 관계 없이 이 공개 방에 있는 모든 기기에서 발견될 수 있습니다.\",\n        \"traffic\": \"WebRTC가 사용 불가능한 경우\",\n        \"routed\": \"트래픽이\",\n        \"webrtc\": \"서버를 경유합니다.\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"PairDrop에 대하여 닫기\",\n        \"claim\": \"기기 간 파일을 주고받는 가장 쉬운 방법\",\n        \"github_title\": \"GitHub의 PairDrop\",\n        \"buy-me-a-coffee_title\": \"Buy me a coffee!\",\n        \"tweet_title\": \"PairDrop에 대해 트윗하기\",\n        \"mastodon_title\": \"PairDrop에 대해 Mastodon에 글 남기기\",\n        \"bluesky_title\": \"BlueSky에서 팔로우하기\",\n        \"custom_title\": \"팔로우하기\",\n        \"privacypolicy_title\": \"개인정보처리방침 열기\",\n        \"faq_title\": \"자주 묻는 질문\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"파일 받음\",\n        \"file-transfer-requested\": \"파일 전송 요청 받음\",\n        \"image-transfer-requested\": \"이미지 전송 요청 받음\",\n        \"message-received\": \"메시지 받음\",\n        \"message-received-plural\": \"메시지 {{count}}개 받음\",\n        \"file-received-plural\": \"파일 {{count}}개 받음\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"클릭하여 {{descriptor}} 전송하기\",\n        \"click-to-send\": \"클릭하여 파일을 보내거나 오른쪽 클릭하여 메시지 보내기\",\n        \"waiting\": \"대기 중…\",\n        \"processing\": \"처리 중…\",\n        \"connection-hash\": \"종단간 암호화의 보안을 검증하려면 양쪽의 장치에서 이 보안 번호를 비교하세요\",\n        \"preparing\": \"준비 중…\",\n        \"transferring\": \"전송 중…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/nb.json",
    "content": "{\n    \"header\": {\n        \"edit-paired-devices_title\": \"Rediger sammenkoblede enheter\",\n        \"about_title\": \"Om PairDrop\",\n        \"about_aria-label\": \"Åpne «Om PairDrop»\",\n        \"theme-auto_title\": \"Juster drakt til system automatisk\",\n        \"theme-light_title\": \"Alltid bruk lys drakt\",\n        \"theme-dark_title\": \"Alltid bruk mørk drakt\",\n        \"notification_title\": \"Skru på varslinger\",\n        \"cancel-share-mode\": \"Ferdig\",\n        \"install_title\": \"Installer PairDrop\",\n        \"pair-device_title\": \"Sammenkoble dine enheter permanent\",\n        \"language-selector_title\": \"Velg språk\",\n        \"edit-share-mode\": \"Rediger\",\n        \"expand_title\": \"Utvid overskriftknapprad\",\n        \"join-public-room_title\": \"Bli med i et offentlig rom midlertidig\"\n    },\n    \"footer\": {\n        \"webrtc\": \"hvis WebRTC ikke er tilgjengelig.\",\n        \"display-name_data-placeholder\": \"Laster inn…\",\n        \"display-name_title\": \"Rediger ditt enhetsnavn permanent\",\n        \"traffic\": \"Trafikken\",\n        \"on-this-network\": \"på dette nettverket\",\n        \"known-as\": \"Du er kjent som:\",\n        \"paired-devices\": \"av sammenkoblede enheter\",\n        \"routed\": \"Sendes gjennom tjeneren\",\n        \"discovery\": \"Du kan bli oppdaget:\",\n        \"on-this-network_title\": \"Du kan bli oppdaget av alle på dette nettverket.\",\n        \"paired-devices_title\": \"Du kan alltid bli oppdaget av sammenkoblede enheter uavhengig av nettverk.\",\n        \"public-room-devices_title\": \"Du kan bli oppdaget av enheter i dette offentlige rommet uavhengig av nettverk.\",\n        \"public-room-devices\": \"i rom {{roomId}}\"\n    },\n    \"instructions\": {\n        \"x-instructions_desktop\": \"Klikk for å sende filer, eller høyreklikk for å sende en melding\",\n        \"x-instructions_mobile\": \"Trykk for å sende filer, eller lang-trykk for å sende en melding\",\n        \"x-instructions_data-drop-bg\": \"Slipp for å velge mottager\",\n        \"x-instructions-share-mode_desktop\": \"Klikk for å sende {{descriptor}}\",\n        \"no-peers_data-drop-bg\": \"Slipp for å velge mottager\",\n        \"no-peers-title\": \"Åpne PairDrop på andre enheter for å sende filer\",\n        \"no-peers-subtitle\": \"Sammenkoble enheter eller bli med i et offentlig rom for å kunne oppdages på andre nettverk\",\n        \"x-instructions_data-drop-peer\": \"Slipp for å sende til likemann\",\n        \"x-instructions-share-mode_mobile\": \"Trykk for å sende {{descriptor}}\",\n        \"activate-share-mode-base\": \"Åpne PairDrop på andre enheter for å sende\",\n        \"activate-share-mode-and-other-files-plural\": \"og {{count}} andre filer\",\n        \"activate-share-mode-shared-text\": \"delt tekst\",\n        \"activate-share-mode-and-other-file\": \"og 1 annen fil\",\n        \"activate-share-mode-shared-file\": \"delt fil\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} delte filer\",\n        \"webrtc-requirement\": \"For å bruke denne PairDrop-økten, må WebRTC være aktivert!\"\n    },\n    \"dialogs\": {\n        \"input-key-on-this-device\": \"Skriv inn denne nøkkelen på en annen enhet\",\n        \"pair-devices-title\": \"Sammenkoble Enheter Permanent\",\n        \"would-like-to-share\": \"ønsker å dele\",\n        \"auto-accept-instructions-2\": \"for å godkjenne alle filer sendt fra den enheten automatisk.\",\n        \"paired-devices-wrapper_data-empty\": \"Ingen sammenkoblede enheter.\",\n        \"enter-key-from-another-device\": \"Skriv inn nøkkel fra en annen enhet her.\",\n        \"edit-paired-devices-title\": \"Rediger Sammenkoblede Enheter\",\n        \"accept\": \"Godta\",\n        \"has-sent\": \"har sendt:\",\n        \"base64-paste-to-send\": \"Lim inn her for å dele {{type}}\",\n        \"base64-text\": \"tekst\",\n        \"base64-files\": \"filer\",\n        \"file-other-description-image-plural\": \"og {{count}} andre bilder\",\n        \"receive-title\": \"{{descriptor}} mottatt\",\n        \"send-message-title\": \"Send melding\",\n        \"base64-processing\": \"Behandler…\",\n        \"close\": \"Lukk\",\n        \"decline\": \"Avslå\",\n        \"download\": \"Last ned\",\n        \"copy\": \"Kopier\",\n        \"pair\": \"Sammenkoble\",\n        \"cancel\": \"Avbryt\",\n        \"scan-qr-code\": \"eller skann QR-koden.\",\n        \"auto-accept-instructions-1\": \"Aktiver\",\n        \"receive-text-title\": \"Melding mottatt\",\n        \"auto-accept\": \"auto-godkjenn\",\n        \"share\": \"Del\",\n        \"send-message-to\": \"Til:\",\n        \"send\": \"Send\",\n        \"base64-tap-to-paste\": \"Trykk her for å dele {{type}}\",\n        \"file-other-description-image\": \"og ett annet bilde\",\n        \"file-other-description-file-plural\": \"og {{count}} andre filer\",\n        \"title-file-plural\": \"Filer\",\n        \"download-again\": \"Last ned igjen\",\n        \"file-other-description-file\": \"og én annen fil\",\n        \"title-image\": \"Bilde\",\n        \"title-file\": \"Fil\",\n        \"title-image-plural\": \"Bilder\",\n        \"join\": \"Bli med\",\n        \"share-text-checkbox\": \"Alltid vis denne dialogen ved deling av tekst\",\n        \"language-selector-title\": \"Velg Språk\",\n        \"unpair\": \"Fjern sammenkobling\",\n        \"temporary-public-room-title\": \"Midlertidig Offentlig Rom\",\n        \"input-room-id-on-another-device\": \"Legg inn denne rom-IDen på en annen enhet\",\n        \"hr-or\": \"ELLER\",\n        \"leave\": \"Forlat\",\n        \"paired-device-removed\": \"Sammenkoblet enhet har blitt fjernet.\",\n        \"message_title\": \"Sett inn meldingen du vil sende\",\n        \"message_placeholder\": \"Tekst\",\n        \"base64-title-files\": \"Delte filer\",\n        \"system-language\": \"Systemspråk\",\n        \"public-room-qr-code_title\": \"Trykk for å kopiere lenke til offentlig rom\",\n        \"pair-devices-qr-code_title\": \"Trykk for å kopiere lenken til å sammenkoble denne enheten\",\n        \"approve\": \"godkjenn\",\n        \"share-text-title\": \"Del Tekstmelding\",\n        \"share-text-subtitle\": \"Rediger melding før sending:\",\n        \"close-toast_title\": \"Lukk varsel\",\n        \"enter-room-id-from-another-device\": \"Legg inn rom-ID fra en annen enhet for å bli med i rommet.\",\n        \"base64-title-text\": \"Delt Tekst\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"Lukk «Om PairDrop»\",\n        \"faq_title\": \"Ofte stilte spørsmål\",\n        \"claim\": \"Den enkleste måten å overføre filer mellom enheter\",\n        \"buy-me-a-coffee_title\": \"Spander drikke!\",\n        \"tweet_title\": \"Tvitre om PairDrop\",\n        \"github_title\": \"PairDrop på GitHub\",\n        \"mastodon_title\": \"Skriv om PairDrop på Mastadon\",\n        \"bluesky_title\": \"Følg oss på BlueSky\",\n        \"custom_title\": \"Følg oss\",\n        \"privacypolicy_title\": \"Åpne vår personvernerklæring\"\n    },\n    \"notifications\": {\n        \"copied-to-clipboard\": \"Kopiert til utklippstavlen\",\n        \"pairing-tabs-error\": \"Sammenkobling av to nettleserfaner er ikke mulig\",\n        \"notifications-enabled\": \"Merknader påskrudd\",\n        \"click-to-show\": \"Klikk for å vise\",\n        \"copied-text\": \"Tekst kopiert til utklippstavlen\",\n        \"connected\": \"Tilkoblet\",\n        \"online\": \"Du er tilbake på nett\",\n        \"file-transfer-completed\": \"Filoverføring utført\",\n        \"selected-peer-left\": \"Valgt likemann dro\",\n        \"pairing-key-invalid\": \"Ugyldig nøkkel\",\n        \"connecting\": \"Kobler til…\",\n        \"pairing-not-persistent\": \"Sammenkoblede enheter er ikke vedvarende\",\n        \"offline\": \"Du er frakoblet\",\n        \"online-requirement\": \"Du må være på nett for å koble sammen enheter.\",\n        \"display-name-random-again\": \"Visningsnavnet er tilfeldig generert igjen\",\n        \"display-name-changed-permanently\": \"Visningsnavnet er endret for godt\",\n        \"display-name-changed-temporarily\": \"Visningsnavnet er endret kun for denne økten\",\n        \"text-content-incorrect\": \"Tekstinnholdet er uriktig\",\n        \"file-content-incorrect\": \"Filinnholdet er uriktig\",\n        \"click-to-download\": \"Klikk for å laste ned\",\n        \"message-transfer-completed\": \"Meldingsoverføring utført\",\n        \"download-successful\": \"{{descriptor}} nedlastet\",\n        \"pairing-success\": \"Enheter sammenkoblet\",\n        \"pairing-cleared\": \"Sammenkobling av alle enheter opphevet\",\n        \"pairing-key-invalidated\": \"Nøkkel {{key}} ugyldiggjort\",\n        \"copied-text-error\": \"Kunne ikke legge innhold i utklippstavlen. Kopier manuelt!\",\n        \"clipboard-content-incorrect\": \"Utklippstavleinnholdet er uriktig\",\n        \"link-received\": \"Lenke mottatt av {{name}} - Klikk for å åpne\",\n        \"request-title\": \"{{name}} ønsker å overføre {{count}} {{descriptor}}\",\n        \"message-received\": \"Melding mottatt av {{name}} - Klikk for å åpne\",\n        \"files-incorrect\": \"Filene er uriktige\",\n        \"ios-memory-limit\": \"Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen\",\n        \"unfinished-transfers-warning\": \"Det er ufullførte overføringer. Er du sikker på at du vil lukke PairDrop?\",\n        \"rate-limit-join-key\": \"Grense nådd. Vent 10 sekunder og prøv igjen.\",\n        \"copied-to-clipboard-error\": \"Kopiering ikke mulig, Kopier manuelt.\",\n        \"public-room-id-invalid\": \"Ugyldig rom-ID\",\n        \"public-room-left\": \"Forlot offentlig rom {{publicRoomId}}\",\n        \"room-url-copied-to-clipboard\": \"Lenke for offentlig rom kopiert til utklippstavle\",\n        \"online-requirement-pairing\": \"Du må være på nett for å sammenkoble enheter\",\n        \"online-requirement-public-room\": \"Du må være på nett for å opprette et offentlig rom\",\n        \"pair-url-copied-to-clipboard\": \"Lenke for sammenkobling til denne enheten kopiert til utklipstavle\",\n        \"notifications-permissions-error\": \"Varlseltillatelse har blitt blokkert fordi brukeren har avvist forespørselen flere ganger. Dette kan tilbakestilles i Sideinnformasjon som kan bli funnet ved å trykke på låseikonet ved siden av URL-feltet.\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Fil mottatt\",\n        \"file-received-plural\": \"{{count}} filer mottatt\",\n        \"message-received\": \"Melding mottatt\",\n        \"file-transfer-requested\": \"Filoverføring forespurt\",\n        \"message-received-plural\": \"{{count}} meldinger mottatt\",\n        \"image-transfer-requested\": \"Blideoverføring forespurt\"\n    },\n    \"peer-ui\": {\n        \"preparing\": \"Forbereder…\",\n        \"waiting\": \"Venter…\",\n        \"processing\": \"Behandler…\",\n        \"transferring\": \"Overfører…\",\n        \"click-to-send\": \"Klikk for å sende filer, eller høyreklikk for å sende en melding\",\n        \"click-to-send-share-mode\": \"Klikk for å sende {{descriptor}}\",\n        \"connection-hash\": \"Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen\"\n    }\n}\n"
  },
  {
    "path": "public/lang/nl.json",
    "content": "{\n    \"footer\": {\n        \"webrtc\": \"als WebRTC niet beschikbaar is.\",\n        \"public-room-devices_title\": \"U kan door apparaten gevonden worden in deze openbare ruimte, ongeacht van het netwerk.\",\n        \"display-name_data-placeholder\": \"Laden…\",\n        \"display-name_title\": \"Bewerk uw apparaatnaam permanent\",\n        \"traffic\": \"Dataverkeer is\",\n        \"paired-devices_title\": \"U kan gevonden worden door gekoppelde apparaten, ongeacht van het netwerk.\",\n        \"public-room-devices\": \"in kamer {{roomId}}\",\n        \"paired-devices\": \"door gekoppelde apparaten\",\n        \"on-this-network\": \"op dit netwerk\",\n        \"routed\": \"door de server geleid\",\n        \"discovery\": \"U bent zichtbaar:\",\n        \"on-this-network_title\": \"U kan door iedereen gevonden worden op dit netwerk.\",\n        \"known-as\": \"U staat bekend als:\"\n    },\n    \"notifications\": {\n        \"request-title\": \"{{name}} zou graag {{count}}{{descriptor}} overdragen\",\n        \"unfinished-transfers-warning\": \"Nog niet alle overdrachten zijn compleet. Weet u zeker dat u PairDrop sluiten?\",\n        \"message-received\": \"Bericht ontvangen van {{name}} - Klik om te kopiëren\",\n        \"rate-limit-join-key\": \"Tempolimiet bereikt. Wacht 10 seconde en probeer opnieuw.\",\n        \"connecting\": \"Verbinden…\",\n        \"pairing-key-invalidated\": \"Sleutel {{key}} ongeldig\",\n        \"pairing-key-invalid\": \"Ongeldige sleutel\",\n        \"connected\": \"Verbonden\",\n        \"pairing-not-persistent\": \"Gekoppelde apparaten zijn niet persistent\",\n        \"text-content-incorrect\": \"Tekst inhoud is incorrect\",\n        \"message-transfer-completed\": \"Berichtsoverdracht compleet\",\n        \"file-transfer-completed\": \"Bestandsoverdracht compleet\",\n        \"file-content-incorrect\": \"Bestandsinhoud is incorrect\",\n        \"files-incorrect\": \"Bestanden zijn incorrect\",\n        \"selected-peer-left\": \"Gekozen peer is vertrokken\",\n        \"link-received\": \"Link van {{name}} ontvangen - Klik om te openen\",\n        \"online\": \"U bent terug online\",\n        \"public-room-left\": \"Openbare ruimte {{publicRoomId}} verlaten\",\n        \"copied-text\": \"Tekst naar klembord gekopieërd\",\n        \"display-name-random-again\": \"De weergavenaam is opnieuw willekeurig gegenereerd\",\n        \"display-name-changed-permanently\": \"De weergavenaam is permanent gewijzigd\",\n        \"copied-to-clipboard-error\": \"Kopiëren is niet mogelijk. Kopieer handmatig.\",\n        \"pairing-success\": \"Apparaten gekoppeld\",\n        \"clipboard-content-incorrect\": \"De inhoud van het klembord is incorrect\",\n        \"display-name-changed-temporarily\": \"De weergavenaam is alleen voor deze sessie gewijzigd\",\n        \"copied-to-clipboard\": \"Gekopieerd naar klembord\",\n        \"offline\": \"U bent offline\",\n        \"pairing-tabs-error\": \"Twee webbrowser tabbladen koppelen in is onmogelijk\",\n        \"public-room-id-invalid\": \"Ongeldig kamer ID\",\n        \"click-to-download\": \"Klik om te downloaden\",\n        \"pairing-cleared\": \"Alle apparaten ontkoppeld\",\n        \"notifications-enabled\": \"Meldingen geactiveerd\",\n        \"online-requirement-pairing\": \"U moet online zijn om apparaten te koppelen\",\n        \"ios-memory-limit\": \"Bestandsoverdrachten naar iOS kunnen slechts met 200 MB per keer\",\n        \"online-requirement-public-room\": \"U moet online zijn om een openbare kamer te maken\",\n        \"copied-text-error\": \"Schrijven naar klembord mislukt. Kopieer handmatig!\",\n        \"download-successful\": \"{{descriptor}} downloaden\",\n        \"click-to-show\": \"Klik om te tonen\",\n        \"pair-url-copied-to-clipboard\": \"Koppeling om dit apparaat te koppelen is gekopieerd naar het klembord\",\n        \"room-url-copied-to-clipboard\": \"Koppeling naar openbare kamer gekopieerd naar klembord\",\n        \"notifications-permissions-error\": \"Meldingsmachtiging is geblokkeerd, omdat de gebruiker het machtigingsverzoek meerdere keren heeft afgewezen. Dit kan worden gereset in instellingen van de website, welke toegangelijk is door te klikken op het sloticoon naast de URL-balk.\"\n    },\n    \"header\": {\n        \"cancel-share-mode\": \"Klaar\",\n        \"theme-auto_title\": \"Gebruik systeemstijl\",\n        \"install_title\": \"PairDrop installeren\",\n        \"theme-dark_title\": \"Altijd donkere modus gebruiken\",\n        \"pair-device_title\": \"Koppel uw apparaten permanent\",\n        \"join-public-room_title\": \"Openbare ruimte tijdelijk betreden\",\n        \"notification_title\": \"Meldingen inschakelen\",\n        \"edit-paired-devices_title\": \"Gekoppelde apparaten bewerken\",\n        \"language-selector_title\": \"Taal Selecteren\",\n        \"about_title\": \"Over PairDrop\",\n        \"about_aria-label\": \"Open Over PairDrop\",\n        \"theme-light_title\": \"Altijd lichte modus gebruiken\",\n        \"expand_title\": \"Knoppenrij uitvouwen\",\n        \"edit-share-mode\": \"Bewerken\"\n    },\n    \"instructions\": {\n        \"x-instructions_mobile\": \"Tik om bestanden te versturen of houdt vast om een bericht te sturen\",\n        \"x-instructions-share-mode_desktop\": \"Klik om te verzenden {{descriptor}}\",\n        \"activate-share-mode-and-other-files-plural\": \"en {{count}} andere bestanden\",\n        \"x-instructions-share-mode_mobile\": \"Tik om te verzenden {{descriptor}}\",\n        \"activate-share-mode-base\": \"Open PairDrop op andere apparaten om te verzenden\",\n        \"no-peers-subtitle\": \"Koppel apparaten of betreed een openbare ruimte om op andere netwerken zichtbaar te worden\",\n        \"activate-share-mode-shared-text\": \"gedeelde tekst\",\n        \"x-instructions_desktop\": \"Klik om bestanden te versturen of rechtsklik om een bericht te sturen\",\n        \"no-peers-title\": \"Open PairDrop op andere apparaten om bestanden te versturen\",\n        \"x-instructions_data-drop-peer\": \"Laat los om naar peer te versturen\",\n        \"x-instructions_data-drop-bg\": \"Loslaten om ontvanger te selecteren\",\n        \"no-peers_data-drop-bg\": \"Loslaten om ontvanger te kiezen\",\n        \"activate-share-mode-and-other-file\": \"en 1 ander bestand\",\n        \"activate-share-mode-shared-file\": \"gedeeld bestand\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} gedeelde bestanden\",\n        \"webrtc-requirement\": \"Om deze PairDrop instantie te kunnen gebruiken, moet WebRTC geactiveerd zijn!\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"Verwerken…\",\n        \"click-to-send-share-mode\": \"Klik om {{descriptor}} te versturen\",\n        \"click-to-send\": \"Klik om bestanden te versturen of rechtsklik om een bericht te versturen\",\n        \"waiting\": \"Wachten…\",\n        \"connection-hash\": \"Vergelijk dit veiligheidsnummer op beide apparaten, om de beveiliging van de eind-tot-eind versleuteling te verifiëren\",\n        \"preparing\": \"Voorbereiden…\",\n        \"transferring\": \"Overdragen…\"\n    },\n    \"dialogs\": {\n        \"base64-paste-to-send\": \"Plak klembord hier om {{type}} te versturen\",\n        \"auto-accept-instructions-2\": \"om automatisch alle bestanden van dat apparaat te accepteren.\",\n        \"receive-text-title\": \"Bericht Ontvangen\",\n        \"edit-paired-devices-title\": \"Gekoppelde Apparaten Bewerken\",\n        \"cancel\": \"Annuleer\",\n        \"auto-accept-instructions-1\": \"Activeer\",\n        \"pair-devices-title\": \"Koppel Apparaten Permanent\",\n        \"download\": \"Download\",\n        \"title-file\": \"Bestand\",\n        \"base64-processing\": \"Verwerken…\",\n        \"decline\": \"Afwijzen\",\n        \"receive-title\": \"{{descriptor}} Ontvangen\",\n        \"leave\": \"Verlaten\",\n        \"join\": \"Betreden\",\n        \"title-image-plural\": \"Afbeeldingen\",\n        \"send\": \"Verzenden\",\n        \"base64-tap-to-paste\": \"Hier tikken om {{type}} te delen\",\n        \"base64-text\": \"tekst\",\n        \"copy\": \"Kopiëren\",\n        \"file-other-description-image\": \"en één andere afbeelding\",\n        \"temporary-public-room-title\": \"Tijdelijke Openbare Ruimte\",\n        \"base64-files\": \"bestanden\",\n        \"has-sent\": \"stuurde het volgende:\",\n        \"file-other-description-file\": \"en één ander bestand\",\n        \"close\": \"Sluiten\",\n        \"system-language\": \"Systeemtaal\",\n        \"unpair\": \"Ontkoppel\",\n        \"title-image\": \"Afbeelding\",\n        \"file-other-description-file-plural\": \"en {{count}} andere bestanden\",\n        \"would-like-to-share\": \"zou graag het volgende willen delen\",\n        \"send-message-to\": \"Aan:\",\n        \"language-selector-title\": \"Taal Instellen\",\n        \"pair\": \"Koppel\",\n        \"hr-or\": \"OF\",\n        \"scan-qr-code\": \"of scan de QR-code.\",\n        \"input-key-on-this-device\": \"Voer deze sleutel in op een ander apparaat\",\n        \"download-again\": \"Download opnieuw\",\n        \"accept\": \"Accepteren\",\n        \"paired-devices-wrapper_data-empty\": \"Geen gekoppelde apparaten.\",\n        \"enter-key-from-another-device\": \"Voer hier de sleutel van een ander apparaat in.\",\n        \"share\": \"Delen\",\n        \"auto-accept\": \"automatisch-accepteren\",\n        \"title-file-plural\": \"Bestanden\",\n        \"send-message-title\": \"Bericht Sturen\",\n        \"input-room-id-on-another-device\": \"Voer de kamer ID in op een ander apparaat\",\n        \"file-other-description-image-plural\": \"en {{count}} andere afbeeldingen\",\n        \"enter-room-id-from-another-device\": \"Voer de kamer ID van een ander apparaat hier in.\",\n        \"paired-device-removed\": \"Gekoppelde apparaten zijn verwijderd.\",\n        \"message_title\": \"Voer bericht in om te versturen\",\n        \"message_placeholder\": \"Tekst\",\n        \"base64-title-files\": \"Deel Bestanden\",\n        \"base64-title-text\": \"Deel Tekst\",\n        \"public-room-qr-code_title\": \"Klik, om de link naar deze openbare ruimte te kopiëren\",\n        \"approve\": \"goedkeuren\",\n        \"share-text-title\": \"Gedeeld Tekst Bericht\",\n        \"share-text-checkbox\": \"Deze dialoog altijd tonen bij het delen van tekst\",\n        \"close-toast_title\": \"Bericht sluiten\",\n        \"pair-devices-qr-code_title\": \"Klik om de koppeling te kopiëren om dit apparaat te koppelen\",\n        \"share-text-subtitle\": \"Bewerk bericht voor verzending:\"\n    },\n    \"about\": {\n        \"claim\": \"De makkelijkste manier om bestanden tussen apparaten te versturen\",\n        \"tweet_title\": \"Tweet over PairDrop\",\n        \"close-about_aria-label\": \"Sluit Over PairDrop\",\n        \"buy-me-a-coffee_title\": \"Koop een kopje koffie voor mij!\",\n        \"github_title\": \"PairDrop op GitHub\",\n        \"faq_title\": \"Veel gestelde vragen\",\n        \"mastodon_title\": \"Schrijf over PairDrop op Mastodon\",\n        \"bluesky_title\": \"Volg ons op BlueSky\",\n        \"custom_title\": \"Volg ons\",\n        \"privacypolicy_title\": \"Open ons privacybeleid\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"Bestandsoverdracht verzocht\",\n        \"image-transfer-requested\": \"Afbeeldingsoverdracht verzocht\",\n        \"message-received-plural\": \"{{count}} berichten ontvangen\",\n        \"message-received\": \"Bericht ontvangen\",\n        \"file-received\": \"Bestand ontvangen\",\n        \"file-received-plural\": \"{{count}} bestanden ontvangen\"\n    }\n}\n"
  },
  {
    "path": "public/lang/nn.json",
    "content": "{\n    \"header\": {\n        \"language-selector_title\": \"Språk\",\n        \"theme-light_title\": \"Bruk alltid lyst tema\",\n        \"join-public-room_title\": \"Bli med i offentlege rom midlertidig\",\n        \"cancel-share-mode\": \"Avbryt\",\n        \"expand_title\": \"Utvid hovudknappelinja\",\n        \"about_aria-label\": \"Opne Om PairDrop\",\n        \"about_title\": \"Om PairDrop\",\n        \"theme-dark_title\": \"Bruk alltid mørkt tema\",\n        \"notification_title\": \"Slå på varslingar\",\n        \"install_title\": \"Installer PairDrop\",\n        \"pair-device_title\": \"Par einingane dine permanent\",\n        \"edit-paired-devices_title\": \"Rediger para einingar\",\n        \"edit-share-mode\": \"Rediger\",\n        \"theme-auto_title\": \"Endre tema til systemet automatisk\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Slepp for å velje mottakar\",\n        \"no-peers-title\": \"Opne PairDrop på andre einingar for å sende filer\",\n        \"no-peers-subtitle\": \"Par einingar eller gå inn i eit offentleg rom for å bli søkbar på andre nettverk\",\n        \"x-instructions-share-mode_desktop\": \"Klikk for å sende {{descriptor}}\",\n        \"activate-share-mode-shared-file\": \"delt fil\",\n        \"x-instructions_desktop\": \"Klikk for å sende filer, eller høgreklikk for å sende ei melding\",\n        \"x-instructions_mobile\": \"Trykk for å sende filer eller trykk-og-hald for å sende ei melding\",\n        \"x-instructions_data-drop-peer\": \"Slepp for å sende til part\",\n        \"x-instructions_data-drop-bg\": \"Slepp for å sende til mottakar\",\n        \"x-instructions-share-mode_mobile\": \"Trykk for å sende {{descriptor}}\",\n        \"activate-share-mode-base\": \"Opne PairDrop på andre einingar for å sende\",\n        \"activate-share-mode-shared-text\": \"delt tekst\",\n        \"activate-share-mode-and-other-file\": \"og 1 anna fil\",\n        \"activate-share-mode-and-other-files-plural\": \"og {{count}} andre filer\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} delte filer\",\n        \"webrtc-requirement\": \"For å bruke denne PairDrop-økta, må WebRTC vere aktivt!\"\n    },\n    \"footer\": {\n        \"on-this-network\": \"på dette nettverket\",\n        \"paired-devices\": \"med para einingar\",\n        \"paired-devices_title\": \"Du kan bli funnen av para einingar til ei kvar tid uavhengig av nettverket.\",\n        \"known-as\": \"Du er kjend som:\",\n        \"display-name_title\": \"Rediger einingsnamnet ditt permanent\",\n        \"discovery\": \"Du kan bli funnen:\",\n        \"traffic\": \"Trafikken er\",\n        \"public-room-devices\": \"i rom {{roomId}}\",\n        \"display-name_data-placeholder\": \"Lastar…\",\n        \"on-this-network_title\": \"Du kan bli funnen av alle på dette nettverket.\",\n        \"webrtc\": \"Om WebRTC ikkje er tilgjengeleg.\",\n        \"public-room-devices_title\": \"Du kan bli funnen av einingar i dette offentlege rommet uavhengig av nettverket.\",\n        \"routed\": \"ruta gjennom denne tenaren\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"Par einingar permanent\",\n        \"scan-qr-code\": \"eller skann QR-koden.\",\n        \"auto-accept-instructions-1\": \"Aktiver\",\n        \"auto-accept\": \"auto-aksepter\",\n        \"leave\": \"Forlat\",\n        \"would-like-to-share\": \"har lyst til å dele\",\n        \"accept\": \"Aksepter\",\n        \"share\": \"Del\",\n        \"message_title\": \"Legg inn meldinga du vil sende\",\n        \"message_placeholder\": \"Tekst\",\n        \"send\": \"Send\",\n        \"base64-title-files\": \"Dele filer\",\n        \"base64-text\": \"tekst\",\n        \"file-other-description-image\": \"og 1 anna bilete\",\n        \"file-other-description-file\": \"og 1 anna fil\",\n        \"title-image-plural\": \"Bilete\",\n        \"share-text-title\": \"Del tekstmelding\",\n        \"share-text-subtitle\": \"Rediger melding før sending:\",\n        \"share-text-checkbox\": \"Alltid vis denne dialogen når du delar tekst\",\n        \"pair\": \"Par\",\n        \"input-key-on-this-device\": \"Legg inn denne nøkkelen på ei anna eining\",\n        \"temporary-public-room-title\": \"Midlertidig offentleg rom\",\n        \"cancel\": \"Avbryt\",\n        \"has-sent\": \"har sendt:\",\n        \"enter-key-from-another-device\": \"Legg inn nøkkel frå anna eining her.\",\n        \"send-message-title\": \"Send melding\",\n        \"input-room-id-on-another-device\": \"Legg inn denne rom-identen på ei anna eining\",\n        \"enter-room-id-from-another-device\": \"Legg inn rom-ident frå anna eining for å bli med inn i rommet.\",\n        \"close\": \"Lukk\",\n        \"join\": \"Bli med\",\n        \"base64-title-text\": \"Dele tekst\",\n        \"hr-or\": \"ELLER\",\n        \"unpair\": \"Slett paring\",\n        \"send-message-to\": \"Til:\",\n        \"edit-paired-devices-title\": \"Rediger para einingar\",\n        \"paired-devices-wrapper_data-empty\": \"Ingen para einingar.\",\n        \"auto-accept-instructions-2\": \"for å automatisk akseptere alle filer som er sendt til denne eininga.\",\n        \"decline\": \"Avslå\",\n        \"download\": \"Last ned\",\n        \"receive-text-title\": \"Melding mottatt\",\n        \"file-other-description-image-plural\": \"og {{count}} andre bilete\",\n        \"copy\": \"Kopier\",\n        \"base64-processing\": \"Handsamar…\",\n        \"base64-files\": \"filer\",\n        \"language-selector-title\": \"Sett språk\",\n        \"system-language\": \"Systemspråk\",\n        \"base64-tap-to-paste\": \"Trykk her for å dele {{type}}\",\n        \"file-other-description-file-plural\": \"og {{count}} andre filer\",\n        \"base64-paste-to-send\": \"Lim inn her for å dele {{type}}\",\n        \"receive-title\": \"{{descriptor}} motteke\",\n        \"pair-devices-qr-code_title\": \"Klikk for å kopiere lenkje til å pare denne eininga\",\n        \"title-image\": \"Bilete\",\n        \"title-file\": \"Fil\",\n        \"title-file-plural\": \"Filer\",\n        \"download-again\": \"Last ned igjen\",\n        \"public-room-qr-code_title\": \"Klikk for å kopiere lenkje til offentleg rom\",\n        \"close-toast_title\": \"Lukk varslinga\",\n        \"approve\": \"godta\",\n        \"paired-device-removed\": \"Para eining har blitt fjerna.\"\n    },\n    \"about\": {\n        \"claim\": \"Den enklaste måten å sende filer mellom einingar\",\n        \"privacypolicy_title\": \"Opne personvernerklæringa vår\",\n        \"faq_title\": \"Ofte spurde spørsmål\",\n        \"close-about_aria-label\": \"Lukk Om PairDrop\",\n        \"buy-me-a-coffee_title\": \"Kjøp meg ein kaffi!\",\n        \"github_title\": \"PairDrop på GitHub\",\n        \"tweet_title\": \"Tvitre om PairDrop\",\n        \"mastodon_title\": \"Skriv om PairDrop på Masrodon\",\n        \"bluesky_title\": \"Følg oss på BlueSky\",\n        \"custom_title\": \"Følg oss\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"Profilnamnet er endra permanent\",\n        \"pairing-not-persistent\": \"Para einingar er ikkje vedvarande\",\n        \"pairing-success\": \"Einingane er para\",\n        \"pairing-key-invalidated\": \"Nøkkel {{key}} er oppheva\",\n        \"pairing-cleared\": \"Alle einingar er upara\",\n        \"public-room-left\": \"Forlot offentleg rom {{publicRoomId}}\",\n        \"pair-url-copied-to-clipboard\": \"Lenkje for å pare denne eininga er kopiert til utklyppstavla\",\n        \"room-url-copied-to-clipboard\": \"Lenkje til offentleg rom er kopiert til utklyppstavla\",\n        \"clipboard-content-incorrect\": \"Utklyppstavle-innhald er ikkje rett\",\n        \"notifications-enabled\": \"Varslingar skrudd på\",\n        \"link-received\": \"Lenkje motteke av {{name}} - Klikk for å opne\",\n        \"click-to-download\": \"Klikk for å laste ned\",\n        \"click-to-show\": \"Klikk for å vise\",\n        \"online\": \"Du er tilkopla igjen\",\n        \"connected\": \"Tilkopla\",\n        \"copied-text\": \"Kopiert tekst til utklyppstavla\",\n        \"online-requirement-public-room\": \"Du må vere tilkopla for å lage eit offentleg rom\",\n        \"files-incorrect\": \"Filene er feil\",\n        \"file-transfer-completed\": \"Filoverføring er fullført\",\n        \"ios-memory-limit\": \"Sending av filer til iOS er berre mogleg opp til 200 MB på ein gong\",\n        \"rate-limit-join-key\": \"Grense nådd. Vent 10 sekund og prøv om att.\",\n        \"selected-peer-left\": \"Vald brukar har forlatt\",\n        \"copied-text-error\": \"Skriving til utklyppsverktøy feila, kopier manuelt!\",\n        \"connecting\": \"Koplar til…\",\n        \"offline\": \"Du er ikkje tilkopla\",\n        \"online-requirement-pairing\": \"Du må vere tilkopla for å pare einingar\",\n        \"message-transfer-completed\": \"Meldingsoverføring er ferdig\",\n        \"unfinished-transfers-warning\": \"Det er uferdige overføringar. Er du sikker på at du vil late att PairDrop?\",\n        \"display-name-changed-temporarily\": \"Profilnamnet er endra for denne sesjonen\",\n        \"display-name-random-again\": \"Profilnamnet er tilfeldig generert igjen\",\n        \"download-successful\": \"{{descriptor}} lasta ned\",\n        \"pairing-tabs-error\": \"Å pare to nettleser-faner er ikkje mogleg\",\n        \"pairing-key-invalid\": \"Ugyldig nøkkel\",\n        \"public-room-id-invalid\": \"Ugyldig rom-ident\",\n        \"copied-to-clipboard\": \"Kopiert til utklyppstavle\",\n        \"copied-to-clipboard-error\": \"Kopiering ikkje mogleg. Kopier manuelt.\",\n        \"text-content-incorrect\": \"Tekstinnhald er feil\",\n        \"file-content-incorrect\": \"Filinnhald er ikkje rett\",\n        \"notifications-permissions-error\": \"Varslingstillatelse er blokkert fordi brukaren har avvist tillatelsesførespurnaden fleire gonger. Dette kan stillast tilbake i Sideinformasjon, som kan opnast ved å klikke på låsikonet ved sida av URL-feltet.\",\n        \"message-received\": \"Melding motteke av {{name}} - Klikk for å kopiere\",\n        \"request-title\": \"{{name}} vil overføre {{count}} {{descriptor}}\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Fil mottatt\",\n        \"image-transfer-requested\": \"Biletoverføring førespurd\",\n        \"file-received-plural\": \"{{count}} Filer Mottatt\",\n        \"message-received-plural\": \"{{count}} Meldingar motteke\",\n        \"file-transfer-requested\": \"Filoverføring førespurd\",\n        \"message-received\": \"Melding mottatt\"\n    },\n    \"peer-ui\": {\n        \"connection-hash\": \"For å verifisere sikkerheita til ende-til-ende krypteringa, samanlikn dette sikkerheitsnummeret på begge einingane\",\n        \"preparing\": \"Førebur…\",\n        \"transferring\": \"Overfører…\",\n        \"click-to-send-share-mode\": \"Klikk for å sende {{descriptor}}\",\n        \"click-to-send\": \"Klikk for å sende filer eller høgreklikk for å sende ei melding\",\n        \"processing\": \"Prosesserar…\",\n        \"waiting\": \"Ventar…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/pl.json",
    "content": "{\n    \"header\": {\n        \"language-selector_title\": \"Ustaw język\",\n        \"about_aria-label\": \"Otwórz \\\"O PairDrop\\\"\",\n        \"theme-light_title\": \"Zawsze używaj jasnego motywu\",\n        \"theme-dark_title\": \"Zawsze używaj ciemnego motywu\",\n        \"pair-device_title\": \"Sparuj swoje urządzenia na stałe\",\n        \"edit-paired-devices_title\": \"Edytuj sparowane urządzenia\",\n        \"join-public-room_title\": \"Dołącz tymczasowo do pokoju publicznego\",\n        \"cancel-share-mode\": \"Anuluj\",\n        \"edit-share-mode\": \"Edytuj\",\n        \"expand_title\": \"Rozwiń rząd przycisków\",\n        \"about_title\": \"O PairDrop\",\n        \"theme-auto_title\": \"Dostosuj motyw do motywu systemowego\",\n        \"notification_title\": \"Włącz powiadomienia\",\n        \"install_title\": \"Zainstaluj PairDrop\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Zwolnij, aby wybrać odbiorcę\",\n        \"no-peers-title\": \"Otwórz PairDrop na innych urządzeniach, aby wysłać pliki\",\n        \"x-instructions_desktop\": \"Kliknij, aby wysłać pliki lub kliknij prawym przyciskiem myszy, aby wysłać wiadomość\",\n        \"x-instructions_mobile\": \"Naciśnij, aby wysłać pliki lub naciśnij i przytrzymaj, aby wysłać wiadomość\",\n        \"x-instructions_data-drop-bg\": \"Zwolnij, aby wybrać odbiorcę\",\n        \"x-instructions-share-mode_desktop\": \"Kliknij, aby wysłać {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"Naciśnij, aby wysłać {{descriptor}}\",\n        \"activate-share-mode-base\": \"Otwórz PairDrop na innych urządzeniach, aby wysłać\",\n        \"activate-share-mode-and-other-file\": \"i 1 inny plik\",\n        \"activate-share-mode-shared-text\": \"udostępniony tekst\",\n        \"activate-share-mode-shared-file\": \"udostępniony plik\",\n        \"no-peers-subtitle\": \"Sparuj urządzenia lub wejdź do pokoju publicznego, aby być widocznym w innych sieciach\",\n        \"x-instructions_data-drop-peer\": \"Zwolnij, aby wysłać do odbiorcy\",\n        \"activate-share-mode-and-other-files-plural\": \"i {{count}} innych plików\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} udostępnionych plików\",\n        \"webrtc-requirement\": \"Aby móc korzystać z tej instancji PairDrop, należy włączyć WebRTC!\"\n    },\n    \"footer\": {\n        \"known-as\": \"Jesteś widoczny jako:\",\n        \"display-name_data-placeholder\": \"Ładowanie…\",\n        \"display-name_title\": \"Edytuj nazwę swojego urządzenia na stałe\",\n        \"on-this-network\": \"w tej sieci\",\n        \"on-this-network_title\": \"Możesz być znaleziony przez każdego w tej sieci.\",\n        \"paired-devices\": \"przez sparowane urządzenia\",\n        \"discovery\": \"Możesz być wykryty:\",\n        \"paired-devices_title\": \"Możesz zostać wykryty przez sparowane urządzenia przez cały czas, niezależnie od sieci.\",\n        \"public-room-devices\": \"w pokoju {{roomId}}\",\n        \"public-room-devices_title\": \"Możesz zostać wykryty przez urządzenia w tym pokoju publicznym, niezależnie od sieci.\",\n        \"routed\": \"przesyłane przez serwer\",\n        \"webrtc\": \"jeśli WebRTC jest niedostępny.\",\n        \"traffic\": \"Transmisja jest\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"Sparuj Urządzenia Na Stałe\",\n        \"enter-key-from-another-device\": \"Wpisz tu klucz z innego urządzenia.\",\n        \"temporary-public-room-title\": \"Tymczasowy Pokój Publiczny\",\n        \"enter-room-id-from-another-device\": \"Wprowadź ID pokoju z innego urządzenia, aby wejść do pokoju.\",\n        \"hr-or\": \"LUB\",\n        \"pair\": \"Sparuj\",\n        \"cancel\": \"Anuluj\",\n        \"edit-paired-devices-title\": \"Edycja Sparowanych Urządzeń\",\n        \"unpair\": \"Rozłącz sparowanie\",\n        \"paired-devices-wrapper_data-empty\": \"Brak sparowanych urządzeń.\",\n        \"auto-accept-instructions-1\": \"Aktywuj\",\n        \"auto-accept-instructions-2\": \", aby automatycznie przyjąć wszystkie pliki wysłane z tego urządzenia.\",\n        \"auto-accept\": \"samoczynne potwierdzanie\",\n        \"close\": \"Zamknij\",\n        \"join\": \"Dołącz\",\n        \"leave\": \"Opuść\",\n        \"accept\": \"Akceptuj\",\n        \"decline\": \"Odrzuć\",\n        \"has-sent\": \"wysłał:\",\n        \"share\": \"Udostępnij\",\n        \"send-message-title\": \"Wyślij Wiadomość\",\n        \"send-message-to\": \"Do:\",\n        \"message_title\": \"Wprowadź wiadomość do wysłania\",\n        \"message_placeholder\": \"Tekst\",\n        \"receive-text-title\": \"Wiadomość Odebrana\",\n        \"copy\": \"Kopiuj\",\n        \"base64-title-files\": \"Udostępnij Pliki\",\n        \"base64-title-text\": \"Udostępnij Tekst\",\n        \"base64-processing\": \"Przetwarzanie…\",\n        \"base64-tap-to-paste\": \"Naciśnij tutaj, aby udostępnić {{type}}\",\n        \"base64-text\": \"tekst\",\n        \"base64-files\": \"pliki\",\n        \"file-other-description-image\": \"i 1 inny obraz\",\n        \"file-other-description-image-plural\": \"i {{count}} innych obrazów\",\n        \"file-other-description-file-plural\": \"i {{count}} innych plików\",\n        \"title-image\": \"Obraz\",\n        \"title-image-plural\": \"Obrazy\",\n        \"title-file-plural\": \"Pliki\",\n        \"receive-title\": \"{{descriptor}} Otrzymano\",\n        \"download-again\": \"Pobierz ponownie\",\n        \"download\": \"Pobierz\",\n        \"language-selector-title\": \"Ustaw Język\",\n        \"system-language\": \"Język Systemu\",\n        \"file-other-description-file\": \"i 1 inny plik\",\n        \"pair-devices-qr-code_title\": \"Kliknij, aby skopiować odnośnik do sparowania urządzenia\",\n        \"approve\": \"zatwierdź\",\n        \"share-text-subtitle\": \"Edytuj wiadomość przed wysłaniem:\",\n        \"share-text-checkbox\": \"Zawsze pokazuj to okno, gdy udostępniasz tekst\",\n        \"paired-device-removed\": \"Sparowane urządzenie zostało usunięte.\",\n        \"would-like-to-share\": \"chciałby udostępnić\",\n        \"input-key-on-this-device\": \"Wprowadź ten klucz na innym urządzeniu\",\n        \"send\": \"Wyślij\",\n        \"scan-qr-code\": \"lub zeskanuj kod QR.\",\n        \"base64-paste-to-send\": \"Wklej tutaj, aby udostępnić {{type}}\",\n        \"title-file\": \"Plik\",\n        \"input-room-id-on-another-device\": \"Wprowadź ID tego pokoju na innym urządzeniu\",\n        \"public-room-qr-code_title\": \"Kliknij, aby skopiować odnośnik do pokoju publicznego\",\n        \"share-text-title\": \"Udostępnij Wiadomość Tekstową\",\n        \"close-toast_title\": \"Zamknij powiadomienie\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"Zamknij \\\"O PairDrop\\\"\",\n        \"claim\": \"Najłatwiejszy sposób na przesyłanie plików między urządzeniami\",\n        \"buy-me-a-coffee_title\": \"Kup me kawę!\",\n        \"mastodon_title\": \"Napisz o PairDrop na Mastodonie\",\n        \"bluesky_title\": \"Śledź nas na BlueSky\",\n        \"custom_title\": \"Śledź nas\",\n        \"privacypolicy_title\": \"Otwórz naszą Politykę Prywatności\",\n        \"faq_title\": \"Często zadawane pytania (FAQ)\",\n        \"github_title\": \"PairDrop na GitHub\",\n        \"tweet_title\": \"Tweetnij o PairDrop\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"Wyświetlana nazwa została zmieniona na stałe\",\n        \"display-name-random-again\": \"Wyświetlana nazwa została ponownie wygenerowana\",\n        \"download-successful\": \"{{descriptor}} pobrano\",\n        \"pairing-tabs-error\": \"Sparowanie dwóch kart w przeglądarce jest niemożliwe\",\n        \"pairing-success\": \"Sparowane urządzenia\",\n        \"pairing-key-invalid\": \"Nieprawidłowy klucz\",\n        \"pairing-key-invalidated\": \"Klucz {{key}} unieważniony\",\n        \"public-room-id-invalid\": \"Nieprawidłowe ID pokoju\",\n        \"pairing-cleared\": \"Wszystkie urządzenia rozłączone\",\n        \"public-room-left\": \"Opuściłeś pokój publiczny {{publicRoomId}}\",\n        \"copied-to-clipboard-error\": \"Kopiowanie niemożliwe. Skopiuj ręcznie.\",\n        \"text-content-incorrect\": \"Treść tekstu jest nieprawidłowa\",\n        \"file-content-incorrect\": \"Zawartość pliku jest nieprawidłowa\",\n        \"clipboard-content-incorrect\": \"Zawartość schowka jest nieprawidłowa\",\n        \"pair-url-copied-to-clipboard\": \"Link do sparowania tego urządzenia skopiowano do schowka\",\n        \"link-received\": \"Otrzymano link od {{name}} - Kliknij, aby otworzyć\",\n        \"message-received\": \"Otrzymano wiadomość od {{name}} - Kliknij, aby skopiować\",\n        \"click-to-download\": \"Kliknij, aby pobrać\",\n        \"request-title\": \"{{name}} chciałby przesłać {{count}} {{descriptor}}\",\n        \"click-to-show\": \"Kliknij, żeby pokazać\",\n        \"copied-text\": \"Tekst skopiowany do schowka\",\n        \"copied-text-error\": \"Zapis do schowka nie powiódł się. Skopiuj ręcznie!\",\n        \"offline\": \"Jesteś offline\",\n        \"online\": \"Jesteś znów online\",\n        \"connected\": \"Połączony\",\n        \"online-requirement-pairing\": \"Aby sparować urządzenia, musisz być online\",\n        \"online-requirement-public-room\": \"Aby utworzyć pokój publiczny, musisz być online\",\n        \"connecting\": \"Łączenie…\",\n        \"files-incorrect\": \"Pliki są nieprawidłowe\",\n        \"file-transfer-completed\": \"Przesyłanie plików zakończone\",\n        \"ios-memory-limit\": \"Jednorazowe przesyłanie plików do iOS jest możliwe tylko do 200 MB\",\n        \"message-transfer-completed\": \"Przesyłanie wiadomości zakończone\",\n        \"unfinished-transfers-warning\": \"Istnieją niedokończone transfery. Czy na pewno chcesz zamknąć PairDrop?\",\n        \"rate-limit-join-key\": \"Osiągnięto limit. Poczekaj 10 sekund i spróbuj ponownie.\",\n        \"selected-peer-left\": \"Wybrany uczestnik opuścił\",\n        \"display-name-changed-temporarily\": \"Wyświetlana nazwa została zmieniona na czas tej sesji\",\n        \"pairing-not-persistent\": \"Połączenie sparowanych urządzeń nie jest trwałe\",\n        \"copied-to-clipboard\": \"Skopiowano do schowka\",\n        \"room-url-copied-to-clipboard\": \"Link do publicznego pokoju skopiowano do schowka\",\n        \"notifications-enabled\": \"Powiadomienia włączone\",\n        \"notifications-permissions-error\": \"Uprawnienia do powiadomień zostały zablokowane, ponieważ użytkownik kilkakrotnie odrzucił monit o pozwolenie na nie. Można je zresetować w \\\"Informacjach o [stronie]\\\", do których można uzyskać dostęp, klikając ikonę kłódki obok paska adresu URL.\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Plik Odebrany\",\n        \"file-received-plural\": \"{{count}} Plików Odebranych\",\n        \"file-transfer-requested\": \"Zażądano Przesłania Pliku\",\n        \"image-transfer-requested\": \"Zażądano Przesłania Obrazu\",\n        \"message-received\": \"Otrzymano Wiadomość\",\n        \"message-received-plural\": \"{{count}} Wiadomości Odebranych\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Kliknij, aby wysłać {{descriptor}}\",\n        \"click-to-send\": \"Kliknij, aby wysłać pliki lub kliknij prawym przyciskiem myszy, aby wysłać wiadomość\",\n        \"connection-hash\": \"Aby sprawdzić bezpieczeństwo szyfrowania end-to-end, porównaj ten numer bezpieczeństwa na obu urządzeniach\",\n        \"preparing\": \"Przygotowanie…\",\n        \"waiting\": \"Oczekiwanie…\",\n        \"processing\": \"Przetwarzanie…\",\n        \"transferring\": \"Przesyłanie…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/pt-BR.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"Sobre o PairDrop\",\n        \"language-selector_title\": \"Definir idioma\",\n        \"about_aria-label\": \"Abrir Sobre o PairDrop\",\n        \"theme-auto_title\": \"Adaptar o tema ao sistema automaticamente\",\n        \"theme-light_title\": \"Sempre usar o tema claro\",\n        \"theme-dark_title\": \"Sempre usar o tema escuro\",\n        \"notification_title\": \"Ativar notificações\",\n        \"install_title\": \"Instalar o PairDrop\",\n        \"pair-device_title\": \"Emparelhar seus dispositivos permanentemente\",\n        \"edit-paired-devices_title\": \"Editar dispositivos emparelhados\",\n        \"join-public-room_title\": \"Entrar em uma sala pública temporariamente\",\n        \"cancel-share-mode\": \"Cancelar\",\n        \"edit-share-mode\": \"Editar\",\n        \"expand_title\": \"Expandir linha de botões de cabeçalho\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Solte para selecionar o destinatário\",\n        \"no-peers-title\": \"Abra o PairDrop em outros dispositivos para enviar arquivos\",\n        \"no-peers-subtitle\": \"Emparelhe dispositivos ou entre em uma sala pública para ser descoberto em outras redes\",\n        \"x-instructions_desktop\": \"Clique para enviar arquivos ou clique com o botão direito para enviar uma mensagem\",\n        \"x-instructions_mobile\": \"Toque para enviar arquivos ou toque e segure para enviar uma mensagem\",\n        \"x-instructions_data-drop-peer\": \"Solte para enviar para o par\",\n        \"x-instructions_data-drop-bg\": \"Solte para selecionar o destinatário\",\n        \"x-instructions-share-mode_desktop\": \"Clique para enviar {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"Toque para enviar {{descriptor}}\",\n        \"activate-share-mode-base\": \"Abra o PairDrop em outros dispositivos para enviar\",\n        \"activate-share-mode-and-other-files-plural\": \"e {{count}} outros arquivos\",\n        \"activate-share-mode-shared-text\": \"texto compartilhado\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} arquivos compartilhados\",\n        \"activate-share-mode-shared-file\": \"arquivo compartilhado\",\n        \"activate-share-mode-and-other-file\": \"e 1 outro arquivo\",\n        \"webrtc-requirement\": \"Para usar essa instância do PairDrop, o WebRTC deve estar habilitado!\"\n    },\n    \"footer\": {\n        \"known-as\": \"Você é conhecido como:\",\n        \"display-name_data-placeholder\": \"Carregando…\",\n        \"display-name_title\": \"Edite o nome do seu dispositivo permanentemente\",\n        \"discovery\": \"Você pode ser descoberto:\",\n        \"on-this-network\": \"nesta rede\",\n        \"on-this-network_title\": \"Você pode ser descoberto por todos nesta rede.\",\n        \"paired-devices\": \"por dispositivos emparelhados\",\n        \"paired-devices_title\": \"Você pode ser descoberto por dispositivos emparelhados a qualquer momento, independentemente da rede.\",\n        \"public-room-devices\": \"na sala {{roomId}}\",\n        \"public-room-devices_title\": \"Você pode ser descoberto por dispositivos nesta sala pública, independentemente da rede.\",\n        \"traffic\": \"O tráfego é\",\n        \"routed\": \"roteado pelo servidor\",\n        \"webrtc\": \"se o WebRTC não estiver disponível.\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"Emparelhar Dispositivos Permanentemente\",\n        \"input-key-on-this-device\": \"Insira esta chave em outro dispositivo\",\n        \"scan-qr-code\": \"ou escaneie o código QR.\",\n        \"enter-key-from-another-device\": \"Insira a chave de outro dispositivo aqui.\",\n        \"temporary-public-room-title\": \"Sala Pública Temporária\",\n        \"input-room-id-on-another-device\": \"Insira este ID de sala em outro dispositivo\",\n        \"enter-room-id-from-another-device\": \"Insira o ID da sala de outro dispositivo para entrar na sala.\",\n        \"hr-or\": \"OU\",\n        \"pair\": \"Emparelhar\",\n        \"cancel\": \"Cancelar\",\n        \"edit-paired-devices-title\": \"Editar Dispositivos Emparelhados\",\n        \"unpair\": \"Desemparelhar\",\n        \"paired-devices-wrapper_data-empty\": \"Nenhum dispositivo emparelhado.\",\n        \"auto-accept-instructions-1\": \"Ative\",\n        \"auto-accept\": \"auto-aceitar\",\n        \"auto-accept-instructions-2\": \"para aceitar automaticamente todos os arquivos enviados por esse dispositivo.\",\n        \"close\": \"Fechar\",\n        \"join\": \"Entrar\",\n        \"leave\": \"Sair\",\n        \"would-like-to-share\": \"gostaria de compartilhar\",\n        \"accept\": \"Aceitar\",\n        \"decline\": \"Recusar\",\n        \"has-sent\": \"enviou:\",\n        \"share\": \"Compartilhar\",\n        \"download\": \"Baixar\",\n        \"send-message-title\": \"Enviar Mensagem\",\n        \"send-message-to\": \"Para:\",\n        \"message_title\": \"Insira a mensagem a ser enviada\",\n        \"send\": \"Enviar\",\n        \"receive-text-title\": \"Mensagem Recebida\",\n        \"copy\": \"Copiar\",\n        \"base64-processing\": \"Processando…\",\n        \"base64-tap-to-paste\": \"Toque aqui para compartilhar {{type}}\",\n        \"base64-paste-to-send\": \"Cole da área de transferência aqui para compartilhar {{type}}\",\n        \"base64-text\": \"texto\",\n        \"base64-files\": \"arquivos\",\n        \"file-other-description-image\": \"e mais 1 imagem\",\n        \"file-other-description-file\": \"e mais 1 arquivo\",\n        \"file-other-description-image-plural\": \"e mais {{count}} imagens\",\n        \"file-other-description-file-plural\": \"e mais {{count}} arquivos\",\n        \"title-image\": \"Imagem\",\n        \"title-file\": \"Arquivo\",\n        \"title-image-plural\": \"Imagens\",\n        \"title-file-plural\": \"Arquivos\",\n        \"receive-title\": \"{{descriptor}} Recebido\",\n        \"download-again\": \"Baixar novamente\",\n        \"language-selector-title\": \"Definir idioma\",\n        \"system-language\": \"Idioma do sistema\",\n        \"public-room-qr-code_title\": \"Clique para copiar o link da sala pública\",\n        \"pair-devices-qr-code_title\": \"Clique para copiar o link para emparelhar este dispositivo\",\n        \"message_placeholder\": \"Texto\",\n        \"close-toast_title\": \"Fechar notificação\",\n        \"share-text-checkbox\": \"Sempre exibir essa mensagem ao compartilhar texto\",\n        \"base64-title-files\": \"Compartilhar arquivos\",\n        \"approve\": \"aprovar\",\n        \"paired-device-removed\": \"Dispositivo pareado foi removido.\",\n        \"share-text-title\": \"Compartilhar Mensagem de Texto\",\n        \"share-text-subtitle\": \"Editar mensagem antes de enviar:\",\n        \"base64-title-text\": \"Compartilhar texto\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"Fechar Sobre o PairDrop\",\n        \"claim\": \"A maneira mais fácil de transferir arquivos entre dispositivos\",\n        \"github_title\": \"PairDrop no GitHub\",\n        \"buy-me-a-coffee_title\": \"Me compre um café!\",\n        \"tweet_title\": \"Tweet sobre o PairDrop\",\n        \"faq_title\": \"Perguntas frequentes\",\n        \"mastodon_title\": \"Escrever sobre PairDrop no Mastodon\",\n        \"bluesky_title\": \"Siga-nos no BlueSky\",\n        \"custom_title\": \"Siga-nos\",\n        \"privacypolicy_title\": \"Abra nossa política de privacidade\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"O nome de exibição é alterado permanentemente\",\n        \"display-name-changed-temporarily\": \"O nome de exibição é alterado apenas para esta sessão\",\n        \"display-name-random-again\": \"O nome de exibição é gerado aleatoriamente novamente\",\n        \"download-successful\": \"{{descriptor}} baixado\",\n        \"pairing-tabs-error\": \"Emparelhar duas abas do navegador web é impossível\",\n        \"pairing-success\": \"Dispositivos emparelhados\",\n        \"pairing-not-persistent\": \"Dispositivos emparelhados não são persistentes\",\n        \"pairing-key-invalid\": \"Chave inválida\",\n        \"pairing-key-invalidated\": \"Chave {{key}} invalidada\",\n        \"pairing-cleared\": \"Todos os dispositivos desemparelhados\",\n        \"public-room-id-invalid\": \"ID da sala inválido\",\n        \"public-room-left\": \"Saiu da sala pública {{publicRoomId}}\",\n        \"copied-to-clipboard\": \"Copiado para a área de transferência\",\n        \"pair-url-copied-to-clipboard\": \"Link para emparelhar este dispositivo copiado para a área de transferência\",\n        \"room-url-copied-to-clipboard\": \"Link para a sala pública copiado para a área de transferência\",\n        \"copied-to-clipboard-error\": \"Cópia não possível. Copie manualmente.\",\n        \"text-content-incorrect\": \"O conteúdo do texto está incorreto\",\n        \"file-content-incorrect\": \"O conteúdo do arquivo está incorreto\",\n        \"clipboard-content-incorrect\": \"O conteúdo da área de transferência está incorreto\",\n        \"notifications-enabled\": \"Notificações ativadas\",\n        \"notifications-permissions-error\": \"A permissão de notificações foi bloqueada porque o usuário dispensou o prompt de permissão várias vezes. Isso pode ser redefinido nas Informações da Página, que podem ser acessadas clicando no ícone de cadeado ao lado da barra de URL.\",\n        \"link-received\": \"Link recebido por {{name}} - Clique para abrir\",\n        \"message-received\": \"Mensagem recebida por {{name}} - Clique para copiar\",\n        \"click-to-download\": \"Clique para baixar\",\n        \"request-title\": \"{{name}} gostaria de transferir {{count}} {{descriptor}}\",\n        \"click-to-show\": \"Clique para mostrar\",\n        \"copied-text\": \"Texto copiado para a área de transferência\",\n        \"copied-text-error\": \"Escrever na área de transferência falhou. Copie manualmente!\",\n        \"offline\": \"Você está offline\",\n        \"online\": \"Você está online novamente\",\n        \"connected\": \"Conectado\",\n        \"online-requirement-pairing\": \"Você precisa estar online para emparelhar dispositivos\",\n        \"online-requirement-public-room\": \"Você precisa estar online para criar uma sala pública\",\n        \"connecting\": \"Conectando…\",\n        \"files-incorrect\": \"Os arquivos estão incorretos\",\n        \"file-transfer-completed\": \"Transferência de arquivo concluída\",\n        \"ios-memory-limit\": \"Enviar arquivos para iOS só é possível até 200 MB de uma vez\",\n        \"message-transfer-completed\": \"Transferência de mensagem concluída\",\n        \"unfinished-transfers-warning\": \"Há transferências inacabadas. Tem certeza de que deseja fechar o PairDrop?\",\n        \"rate-limit-join-key\": \"Limite de taxa atingido. Aguarde 10 segundos e tente novamente.\",\n        \"selected-peer-left\": \"Par selecionado saiu\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Arquivo recebido\",\n        \"file-received-plural\": \"{{count}} Arquivos recebidos\",\n        \"file-transfer-requested\": \"Transferência de arquivo solicitada\",\n        \"image-transfer-requested\": \"Transferência de imagem solicitada\",\n        \"message-received\": \"Mensagem recebida\",\n        \"message-received-plural\": \"{{count}} mensagens recebidas\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Clique para enviar {{descriptor}}\",\n        \"click-to-send\": \"Clique para enviar arquivos ou clique com o botão direito para enviar uma mensagem\",\n        \"connection-hash\": \"Para verificar a segurança da criptografia de ponta a ponta, compare este número de segurança em ambos os dispositivos\",\n        \"preparing\": \"Preparando…\",\n        \"waiting\": \"Aguardando…\",\n        \"processing\": \"Processando…\",\n        \"transferring\": \"Transferindo…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/ro.json",
    "content": "{\n    \"footer\": {\n        \"webrtc\": \"dacă WebRTC nu este disponibil.\",\n        \"public-room-devices_title\": \"Poți fi descoperit de dispozitivele din această cameră publică, independent de rețea.\",\n        \"display-name_data-placeholder\": \"Se încarcă…\",\n        \"display-name_title\": \"Editați permanent numele dispozitivului tău\",\n        \"traffic\": \"Traficul este\",\n        \"paired-devices_title\": \"Poți fi descoperit în orice moment de dispozitivele cuplate, indiferent de rețea.\",\n        \"public-room-devices\": \"în camera {{roomId}}\",\n        \"paired-devices\": \"prin dispozitive împerecheate\",\n        \"on-this-network\": \"în această rețea\",\n        \"routed\": \"rutate prin server\",\n        \"discovery\": \"Poți fi descoperit:\",\n        \"on-this-network_title\": \"Poți fi descoperit de toată lumea din această rețea.\",\n        \"known-as\": \"Ești cunoscut ca:\"\n    },\n    \"notifications\": {\n        \"request-title\": \"{{name}} ar dori să transfere {{count}} {{descriptor}}\",\n        \"unfinished-transfers-warning\": \"Există transferuri neterminate. Sigur vrei să închizi PairDrop?\",\n        \"message-received\": \"Mesaj primit de {{name}} - Apasă pentru a copia\",\n        \"rate-limit-join-key\": \"A fost atinsă limita ratei. Așteptați 10 secunde și încercați din nou.\",\n        \"connecting\": \"Conectarea…\",\n        \"pairing-key-invalidated\": \"Cheia {{key}} invalidată\",\n        \"pairing-key-invalid\": \"Cheie invalidă\",\n        \"connected\": \"Conectat\",\n        \"pairing-not-persistent\": \"Dispozitivele cuplate nu sunt persistente\",\n        \"text-content-incorrect\": \"Conținutul textului este incorect\",\n        \"message-transfer-completed\": \"Transferul mesajului este finalizat\",\n        \"file-transfer-completed\": \"Transfer de fișiere finalizat\",\n        \"file-content-incorrect\": \"Conținutul fișierului este incorect\",\n        \"files-incorrect\": \"Fișierele sunt incorecte\",\n        \"selected-peer-left\": \"Selectat peer a plecat\",\n        \"link-received\": \"Link primit de {{name}} - Apasă pentru a deschide\",\n        \"online\": \"Ați revenit online\",\n        \"public-room-left\": \"Plecat din camera publică {{publicRoomId}}\",\n        \"copied-text\": \"Text copiat în clipboard\",\n        \"display-name-random-again\": \"Numele afișat este din nou generat aleatoriu\",\n        \"display-name-changed-permanently\": \"Numele afișat este schimbat permanent\",\n        \"copied-to-clipboard-error\": \"Copierea nu este posibilă. Copiați manual.\",\n        \"pairing-success\": \"Dispozitive asociate\",\n        \"clipboard-content-incorrect\": \"Conținutul clipboard-ului este incorect\",\n        \"display-name-changed-temporarily\": \"Numele afișat se modifică numai pentru această sesiune\",\n        \"copied-to-clipboard\": \"Copiat în clipboard\",\n        \"offline\": \"Ești offline\",\n        \"pairing-tabs-error\": \"Cuplarea între două file de browser web este imposibilă\",\n        \"public-room-id-invalid\": \"ID-ul camerei invalid\",\n        \"click-to-download\": \"Apasă pentru a descărca\",\n        \"pairing-cleared\": \"Toate dispozitivele sunt decuplate\",\n        \"notifications-enabled\": \"Notificări activate\",\n        \"online-requirement-pairing\": \"Trebuie să fiți online pentru a asocia dispozitivele\",\n        \"ios-memory-limit\": \"Trimiterea de fișiere pe iOS este posibilă doar până la 200 MB simultan\",\n        \"online-requirement-public-room\": \"Trebuie să fiți online pentru a crea o cameră publică\",\n        \"copied-text-error\": \"Scrierea în clipboard a eșuat. Copiați manual!\",\n        \"download-successful\": \"{{descriptor}} descărcat\",\n        \"click-to-show\": \"Apasă pentru a arăta\",\n        \"notifications-permissions-error\": \"Permisiunea de notificare a fost blocată deoarece utilizatorul a respins de mai multe ori solicitarea de permisiune. Acest lucru poate fi resetat în Info pagină, care poate fi accesat făcând clic pe pictograma de blocare de lângă bara URL.\",\n        \"pair-url-copied-to-clipboard\": \"Link pentru a asocia acest dispozitiv copiat în clipboard\",\n        \"room-url-copied-to-clipboard\": \"Link către sala publică copiat în clipboard\"\n    },\n    \"header\": {\n        \"cancel-share-mode\": \"Anulare\",\n        \"theme-auto_title\": \"Adaptează tema la sistem\",\n        \"install_title\": \"Instalează PairDrop\",\n        \"theme-dark_title\": \"Utilizați mereu tema întunecoasă\",\n        \"pair-device_title\": \"Împerechează-ți permanent dispozitivele\",\n        \"join-public-room_title\": \"Alătură-te temporar camerei publice\",\n        \"notification_title\": \"Activați notificări\",\n        \"edit-paired-devices_title\": \"Editați dispozitivele împerecheate\",\n        \"language-selector_title\": \"Setează Limba\",\n        \"about_title\": \"Despre PairDrop\",\n        \"about_aria-label\": \"Deschide Despre PairDrop\",\n        \"theme-light_title\": \"Utilizați mereu tema luminoasă\",\n        \"expand_title\": \"Extindeți rândul de butoane antet\",\n        \"edit-share-mode\": \"Editați\"\n    },\n    \"instructions\": {\n        \"x-instructions_mobile\": \"Atingeți pentru a trimite fișiere sau atingeți lung pentru a trimite un mesaj\",\n        \"x-instructions-share-mode_desktop\": \"Faceți clic pentru a trimite {{descriptor}}\",\n        \"activate-share-mode-and-other-files-plural\": \"și {{count}} alte fișiere\",\n        \"x-instructions-share-mode_mobile\": \"Atingeți pentru a trimite {{descriptor}}\",\n        \"activate-share-mode-base\": \"Deschideți PairDrop pe alte dispozitive pentru a trimite\",\n        \"no-peers-subtitle\": \"Împerecheați dispozitive sau intrați într-o cameră publică pentru a fi descoperit în alte rețele\",\n        \"activate-share-mode-shared-text\": \"text partajat\",\n        \"x-instructions_desktop\": \"Dați clic pentru a trimite fișiere sau dați clic dreapta pentru a trimite un mesaj\",\n        \"no-peers-title\": \"Deschideți PairDrop pe alte dispozitive pentru a trimite fișiere\",\n        \"x-instructions_data-drop-peer\": \"Eliberare pentru a trimite la peer\",\n        \"x-instructions_data-drop-bg\": \"Eliberați pentru a selecta recipientul\",\n        \"no-peers_data-drop-bg\": \"Eliberați pentru a selecta destinatarul\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} fișiere partajate\",\n        \"activate-share-mode-shared-file\": \"fișier partajat\",\n        \"activate-share-mode-and-other-file\": \"și încă 1 fișier\",\n        \"webrtc-requirement\": \"Pentru a utiliza această instanță PairDrop, WebRTC trebuie să fie activat!\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"Procesarea…\",\n        \"click-to-send-share-mode\": \"Apasă pentru a trimite {{descriptor}}\",\n        \"click-to-send\": \"Apasă pentru a trimite fișiere sau apasă cu butonul din dreapta pentru a trimite un mesaj\",\n        \"waiting\": \"Așteptând…\",\n        \"connection-hash\": \"Pentru a verifica securitatea criptării end-to-end, comparați acest număr de securitate pe ambele dispozitive\",\n        \"preparing\": \"Pregătirea…\",\n        \"transferring\": \"Transferul…\"\n    },\n    \"dialogs\": {\n        \"base64-paste-to-send\": \"Inserați clipboard aici pentru a distribui {{type}}\",\n        \"auto-accept-instructions-2\": \"pentru a accepta automat toate fișierele trimise de la dispozitivul respectiv.\",\n        \"receive-text-title\": \"Mesaj primit\",\n        \"edit-paired-devices-title\": \"Editați dispozitivele asociate\",\n        \"cancel\": \"Anulează\",\n        \"auto-accept-instructions-1\": \"Activează\",\n        \"pair-devices-title\": \"Împerecherea permanentă a dispozitivelor\",\n        \"download\": \"Descarcă\",\n        \"title-file\": \"Fişier\",\n        \"base64-processing\": \"Procesarea…\",\n        \"decline\": \"Declin\",\n        \"receive-title\": \"{{descriptor}} Primit\",\n        \"leave\": \"Pleacă\",\n        \"join\": \"Alătură-te\",\n        \"title-image-plural\": \"Imagini\",\n        \"send\": \"Trimite\",\n        \"base64-tap-to-paste\": \"Atingeți aici pentru a distribui {{type}}\",\n        \"base64-text\": \"text\",\n        \"copy\": \"Copiază\",\n        \"file-other-description-image\": \"și 1 altă imagine\",\n        \"temporary-public-room-title\": \"Cameră publică temporară\",\n        \"base64-files\": \"fişiere\",\n        \"has-sent\": \"a trimis:\",\n        \"file-other-description-file\": \"și 1 alt fișier\",\n        \"close\": \"Închide\",\n        \"system-language\": \"Limba Sistemului\",\n        \"unpair\": \"Decuplează\",\n        \"title-image\": \"Imagine\",\n        \"file-other-description-file-plural\": \"și {{count}} alte fișiere\",\n        \"would-like-to-share\": \"ar dori să împărtășească\",\n        \"send-message-to\": \"La:\",\n        \"language-selector-title\": \"Setați Limba\",\n        \"pair\": \"Cuplu\",\n        \"hr-or\": \"SAU\",\n        \"scan-qr-code\": \"sau scanați codul QR.\",\n        \"input-key-on-this-device\": \"Introduceți această cheie pe un alt dispozitiv\",\n        \"download-again\": \"Descarcă din nou\",\n        \"accept\": \"Acceptă\",\n        \"paired-devices-wrapper_data-empty\": \"Nu sunt dispozitive asociate.\",\n        \"enter-key-from-another-device\": \"Introduceți aici cheia de la un alt dispozitiv.\",\n        \"share\": \"Partajați\",\n        \"auto-accept\": \"auto-acceptare\",\n        \"title-file-plural\": \"Fişiere\",\n        \"send-message-title\": \"Trimite un mesaj\",\n        \"input-room-id-on-another-device\": \"Introduceți acest ID de cameră pe un alt dispozitiv\",\n        \"file-other-description-image-plural\": \"și {{count}} alte imagini\",\n        \"enter-room-id-from-another-device\": \"Introdu ID-ul camerei de pe un alt dispozitiv pentru a intra în cameră.\",\n        \"message_title\": \"Inserați mesajul de trimis\",\n        \"pair-devices-qr-code_title\": \"Dați clic pentru a copia link-ul pentru a asocia acest dispozitiv\",\n        \"public-room-qr-code_title\": \"Dați clic pentru a copia link-ul în sala publică\",\n        \"message_placeholder\": \"Text\",\n        \"close-toast_title\": \"Închideți notificarea\",\n        \"share-text-checkbox\": \"Afișați întotdeauna acest dialog atunci când partajați text\",\n        \"base64-title-files\": \"Distribuie fisiere\",\n        \"approve\": \"aprobă\",\n        \"paired-device-removed\": \"Dispozitivul asociat a fost eliminat.\",\n        \"share-text-title\": \"Partajați un mesaj text\",\n        \"share-text-subtitle\": \"Editați mesajul înainte de a-l trimite:\",\n        \"base64-title-text\": \"Partajați textul\"\n    },\n    \"about\": {\n        \"claim\": \"Cel mai simplu mod de a transfera fișiere între dispozitive\",\n        \"tweet_title\": \"Tweet despre PairDrop\",\n        \"close-about_aria-label\": \"Închide Despre PairDrop\",\n        \"buy-me-a-coffee_title\": \"Cumpără-mi o cafea!\",\n        \"github_title\": \"PairDrop pe GitHub\",\n        \"faq_title\": \"Întrebări frecvente\",\n        \"bluesky_title\": \"Urmărește-ne pe BlueSky\",\n        \"privacypolicy_title\": \"Deschideți politica noastră de confidențialitate\",\n        \"mastodon_title\": \"Scrieți despre PairDrop pe Mastodon\",\n        \"custom_title\": \"Urmăriți-ne\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"Transfer de fișiere cerut\",\n        \"message-received-plural\": \"{{count}}} Mesaje primite\",\n        \"message-received\": \"Mesaj primit\",\n        \"file-received\": \"Fișier Primit\",\n        \"file-received-plural\": \"{{count}} Fișiere Primite\",\n        \"image-transfer-requested\": \"Transfer de imagine solicitat\"\n    }\n}\n"
  },
  {
    "path": "public/lang/ru.json",
    "content": "{\n    \"header\": {\n        \"about_aria-label\": \"О PairDrop\",\n        \"pair-device_title\": \"Связать ваши устройства навсегда\",\n        \"install_title\": \"Установить PairDrop\",\n        \"cancel-share-mode\": \"Выполнено\",\n        \"edit-paired-devices_title\": \"Редактировать связанные устройства\",\n        \"notification_title\": \"Включить уведомления\",\n        \"about_title\": \"О PairDrop\",\n        \"theme-auto_title\": \"Адаптировать тему к системной автоматически\",\n        \"theme-dark_title\": \"Всегда использовать темную тему\",\n        \"theme-light_title\": \"Всегда использовать светлую тему\",\n        \"join-public-room_title\": \"Войти на время в публичную комнату\",\n        \"language-selector_title\": \"Установить язык\",\n        \"edit-share-mode\": \"Редактировать\",\n        \"expand_title\": \"Развернуть ряд кнопок заголовка\"\n    },\n    \"instructions\": {\n        \"x-instructions_desktop\": \"Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение\",\n        \"no-peers_data-drop-bg\": \"Отпустите, чтобы выбрать получателя\",\n        \"x-instructions-share-mode_desktop\": \"Нажмите, чтобы отправить {{descriptor}}\",\n        \"x-instructions_data-drop-bg\": \"Отпустите, чтобы выбрать получателя\",\n        \"x-instructions-share-mode_mobile\": \"Прикоснитесь, чтобы отправить {{descriptor}}\",\n        \"x-instructions_data-drop-peer\": \"Отпустите, чтобы послать узлу\",\n        \"x-instructions_mobile\": \"Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение\",\n        \"no-peers-title\": \"Откройте PairDrop на других устройствах, чтобы отправить файлы\",\n        \"no-peers-subtitle\": \"Свяжите устройства или войдите в публичную комнату, чтобы вас могли обнаружить из других сетей\",\n        \"activate-share-mode-and-other-files-plural\": \"и {{count}} других файлов\",\n        \"activate-share-mode-base\": \"Откройте PairDrop на других устройствах, чтобы отправить\",\n        \"activate-share-mode-shared-text\": \"общий текст\",\n        \"activate-share-mode-and-other-file\": \"и 1 другой файл\",\n        \"activate-share-mode-shared-file\": \"доступный файл\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} доступных файлов\",\n        \"webrtc-requirement\": \"Включите WebRTC, чтобы пользоваться этой копией PairDrop!\"\n    },\n    \"footer\": {\n        \"display-name_data-placeholder\": \"Загрузка…\",\n        \"routed\": \"направляется через сервер\",\n        \"webrtc\": \", если WebRTC недоступен.\",\n        \"traffic\": \"Трафик\",\n        \"paired-devices\": \"связанными устройствами\",\n        \"known-as\": \"Вы известны под именем:\",\n        \"on-this-network\": \"в этой сети\",\n        \"display-name_title\": \"Изменить имя вашего устройства навсегда\",\n        \"public-room-devices_title\": \"Вы можете быть обнаружены устройствами в этой публичной комнате вне зависимости от сети.\",\n        \"paired-devices_title\": \"Вы можете быть обнаружены связанными устройствами в любое время вне зависимости от сети.\",\n        \"public-room-devices\": \"в комнате {{roomId}}\",\n        \"discovery\": \"Вы можете быть обнаружены:\",\n        \"on-this-network_title\": \"Вы можете быть обнаружены кем угодно в этой сети.\"\n    },\n    \"dialogs\": {\n        \"edit-paired-devices-title\": \"Редактировать Связанные Устройства\",\n        \"auto-accept\": \"автоприем\",\n        \"close\": \"Закрыть\",\n        \"decline\": \"Отклонить\",\n        \"share\": \"Поделиться\",\n        \"would-like-to-share\": \"хотел бы поделиться\",\n        \"has-sent\": \"отправил:\",\n        \"paired-devices-wrapper_data-empty\": \"Нет связанных устройств.\",\n        \"download\": \"Скачать\",\n        \"receive-text-title\": \"Сообщение получено\",\n        \"send\": \"Отправить\",\n        \"send-message-to\": \"Кому:\",\n        \"send-message-title\": \"Отправить сообщение\",\n        \"copy\": \"Копировать\",\n        \"base64-files\": \"файлы\",\n        \"base64-paste-to-send\": \"Вставьте, чтобы поделиться {{type}}\",\n        \"base64-processing\": \"Обработка…\",\n        \"base64-tap-to-paste\": \"Нажмите, чтобы поделиться {{type}}\",\n        \"base64-text\": \"текст\",\n        \"title-file\": \"Файл\",\n        \"title-file-plural\": \"Файлы\",\n        \"title-image\": \"Изображение\",\n        \"title-image-plural\": \"Изображения\",\n        \"download-again\": \"Скачать еще раз\",\n        \"auto-accept-instructions-2\": \", чтобы автоматически принимать все файлы, отправленные с того устройства.\",\n        \"enter-key-from-another-device\": \"Введите сюда ключ с другого устройства.\",\n        \"pair-devices-title\": \"Соединить устройства навсегда\",\n        \"input-key-on-this-device\": \"На другом устройстве введите этот ключ\",\n        \"scan-qr-code\": \"или отсканируйте QR-код.\",\n        \"cancel\": \"Отменить\",\n        \"pair\": \"Подключить\",\n        \"accept\": \"Принять\",\n        \"auto-accept-instructions-1\": \"Активировать\",\n        \"file-other-description-file\": \"и 1 другой файл\",\n        \"file-other-description-image-plural\": \"и {{count}} других изображений\",\n        \"file-other-description-image\": \"и 1 другое изображение\",\n        \"file-other-description-file-plural\": \"и {{count}} других файлов\",\n        \"receive-title\": \"{{descriptor}} получен\",\n        \"system-language\": \"Язык системы\",\n        \"unpair\": \"Отвязать\",\n        \"language-selector-title\": \"Установить язык\",\n        \"hr-or\": \"ИЛИ\",\n        \"input-room-id-on-another-device\": \"На другом устройстве введите этот ID комнаты\",\n        \"leave\": \"Покинуть\",\n        \"join\": \"Войти\",\n        \"enter-room-id-from-another-device\": \"Введите ID комнаты с другого устройства, чтобы войти в нее.\",\n        \"temporary-public-room-title\": \"Временная публичная комната\",\n        \"message_title\": \"Вставьте сообщение для отправки\",\n        \"pair-devices-qr-code_title\": \"Нажмите, чтобы скопировать ссылку для привязки этого устройства\",\n        \"public-room-qr-code_title\": \"Нажмите, чтобы скопировать ссылку на публичную комнату\",\n        \"message_placeholder\": \"Текст\",\n        \"paired-device-removed\": \"Связанное устройство удалено.\",\n        \"close-toast_title\": \"Закрыть уведомление\",\n        \"share-text-checkbox\": \"Всегда показывать это окно перед отправкой\",\n        \"base64-title-text\": \"Поделиться текстом\",\n        \"base64-title-files\": \"Поделиться файлами\",\n        \"share-text-subtitle\": \"Отредактировать сообщение перед отправкой:\",\n        \"approve\": \"одобрить\",\n        \"share-text-title\": \"Поделиться сообщением\"\n    },\n    \"about\": {\n        \"close-about-aria-label\": \"Закрыть страницу \\\"О сервисе\\\"\",\n        \"claim\": \"Самый простой способ передачи файлов между устройствами\",\n        \"close-about_aria-label\": \"Закрыть страницу \\\"О сервисе\\\"\",\n        \"buy-me-a-coffee_title\": \"Купить мне кофе!\",\n        \"github_title\": \"PairDrop на GitHub\",\n        \"tweet_title\": \"Твит о PairDrop\",\n        \"faq_title\": \"Часто задаваемые вопросы\",\n        \"mastodon_title\": \"Расскажите о PairDrop на Mastodon\",\n        \"custom_title\": \"Подпишитесь на нас\",\n        \"bluesky_title\": \"Подписаться на BlueSky\",\n        \"privacypolicy_title\": \"Открыть нашу политику приватности\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"Отображаемое имя было изменено навсегда\",\n        \"display-name-random-again\": \"Отображаемое имя сгенерировалось случайным образом снова\",\n        \"pairing-success\": \"Устройства связаны\",\n        \"pairing-tabs-error\": \"Связка двух вкладок браузера невозможна\",\n        \"copied-to-clipboard\": \"Скопировано в буфер обмена\",\n        \"pairing-not-persistent\": \"Связанные устройства непостоянны\",\n        \"link-received\": \"Получена ссылка от {{name}} - нажмите, чтобы открыть\",\n        \"notifications-enabled\": \"Уведомления включены\",\n        \"text-content-incorrect\": \"Содержание текста неверно\",\n        \"message-received\": \"Получено сообщение от {{name}} - нажмите, чтобы скопировать\",\n        \"connected\": \"Подключено\",\n        \"copied-text\": \"Текст скопирован в буфер обмена\",\n        \"online\": \"Вы снова в сети\",\n        \"offline\": \"Вы находитесь вне сети\",\n        \"online-requirement\": \"Для сопряжения устройств вам нужно быть в сети.\",\n        \"files-incorrect\": \"Файлы неверны\",\n        \"message-transfer-completed\": \"Передача сообщения завершена\",\n        \"ios-memory-limit\": \"Отправка файлов на iOS устройства возможна только до 200 МБ за один раз\",\n        \"selected-peer-left\": \"Выбранный узел вышел\",\n        \"request-title\": \"{{name}} хотел бы передать {{count}} {{descriptor}}\",\n        \"rate-limit-join-key\": \"Достигнут предел скорости. Подождите 10 секунд и повторите попытку.\",\n        \"unfinished-transfers-warning\": \"Есть незавершенные передачи. Вы уверены, что хотите закрыть PairDrop?\",\n        \"copied-text-error\": \"Запись в буфер обмена не удалась. Скопируйте вручную!\",\n        \"pairing-cleared\": \"Все устройства отвязаны\",\n        \"pairing-key-invalid\": \"Неверный ключ\",\n        \"pairing-key-invalidated\": \"Ключ {{key}} признан недействительным\",\n        \"click-to-download\": \"Нажмите, чтобы скачать\",\n        \"clipboard-content-incorrect\": \"Содержание буфера обмена неверно\",\n        \"click-to-show\": \"Нажмите, чтобы показать\",\n        \"connecting\": \"Подключение…\",\n        \"download-successful\": \"{{descriptor}} загружен\",\n        \"display-name-changed-temporarily\": \"Отображаемое имя было изменено только для этой сессии\",\n        \"file-content-incorrect\": \"Содержимое файла неверно\",\n        \"file-transfer-completed\": \"Передача файла завершена\",\n        \"public-room-left\": \"Покинуть публичную комнату {{publicRoomId}}\",\n        \"copied-to-clipboard-error\": \"Копирование невозможно. Скопируйте вручную.\",\n        \"public-room-id-invalid\": \"Неверный ID комнаты\",\n        \"online-requirement-pairing\": \"Для связки устройств необходимо находиться быть онлайн\",\n        \"online-requirement-public-room\": \"Для создания публичной комнаты необходимо быть онлайн\",\n        \"notifications-permissions-error\": \"Уведомления были заблокированы, так как пользователь отклонил запрос на их работу несколько раз. Это можно изменить в меню \\\"О странице\\\", которое может быть вызвано нажатием на иконку замочка рядом со строкой адреса сайта.\",\n        \"pair-url-copied-to-clipboard\": \"Ссылка для привязки этого устройства была скопирована в буфер обмена\",\n        \"room-url-copied-to-clipboard\": \"Ссылка на публичную комнату была скопирована в буфер обмена\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Нажмите, чтобы отправить {{descriptor}}\",\n        \"preparing\": \"Подготовка…\",\n        \"transferring\": \"Передача…\",\n        \"processing\": \"Обработка…\",\n        \"waiting\": \"Ожидание…\",\n        \"connection-hash\": \"Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах\",\n        \"click-to-send\": \"Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение\"\n    },\n    \"document-titles\": {\n        \"file-received-plural\": \"{{count}} файлов получено\",\n        \"message-received-plural\": \"{{count}} сообщений получено\",\n        \"file-received\": \"Файл получен\",\n        \"file-transfer-requested\": \"Запрошена передача файлов\",\n        \"message-received\": \"Сообщение получено\",\n        \"image-transfer-requested\": \"Запрошена передача изображений\"\n    }\n}\n"
  },
  {
    "path": "public/lang/sk.json",
    "content": "{\n    \"header\": {\n        \"install_title\": \"Nainštalovať PairDrop\",\n        \"about_title\": \"O službe PairDrop\",\n        \"language-selector_title\": \"Nastaviť jazyk\",\n        \"about_aria-label\": \"Otvoriť \\\"O službe PairDrop\\\"\",\n        \"theme-auto_title\": \"Automaticky prispôsobiť tému systému\",\n        \"edit-share-mode\": \"Upraviť\",\n        \"expand_title\": \"Rozbaliť riadok tlačítka záhlavia\",\n        \"theme-light_title\": \"Vždy použiť svetlú tému\",\n        \"theme-dark_title\": \"Vždy použiť tmavú tému\",\n        \"notification_title\": \"Povoliť upozornenia\",\n        \"pair-device_title\": \"Spárovať zariadenia natrvalo\",\n        \"edit-paired-devices_title\": \"Upraviť spárované zariadenia\",\n        \"join-public-room_title\": \"Dočasne sa pripojiť k verejnej miestnosti\",\n        \"cancel-share-mode\": \"Zrušiť\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Pustite pre vybratie príjemcu\",\n        \"no-peers-title\": \"Otvorte PairDrop na iných zariadeniach pre posielanie súborov\",\n        \"x-instructions_desktop\": \"Kliknite pre poslanie súborov alebo kliknite pravým tlačidlom pre poslanie správy\",\n        \"x-instructions_data-drop-peer\": \"Pustite pre odoslanie\",\n        \"x-instructions_data-drop-bg\": \"Pustením vyberiete príjemcu\",\n        \"activate-share-mode-and-other-file\": \"a 1 ďalší súbor\",\n        \"activate-share-mode-and-other-files-plural\": \"a {{count}} ďalších súborov\",\n        \"activate-share-mode-shared-text\": \"zdieľaný text\",\n        \"activate-share-mode-shared-file\": \"zdieľaný súbor\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} zdieľaných súborov\",\n        \"no-peers-subtitle\": \"Spárujte zariadenia alebo vstúpte do verejnej miestnosti pre viditeľnosť v iných sieťach\",\n        \"x-instructions_mobile\": \"Ťuknite pre poslanie súboru alebo podržte pre poslanie správy\",\n        \"webrtc-requirement\": \"Pre použitie PairDrop-u je nevyhnutné povoliť WebRTC!\",\n        \"x-instructions-share-mode_desktop\": \"Kliknutím odošlete {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"Ťuknutím odošlete {{descriptor}}\",\n        \"activate-share-mode-base\": \"Pre odoslanie otvorte PairDrop na iných zariadeniach\"\n    },\n    \"footer\": {\n        \"known-as\": \"Si pomenovaný ako:\",\n        \"display-name_data-placeholder\": \"Načítava sa…\",\n        \"display-name_title\": \"Trvalo upraviť názov zariadenia\",\n        \"discovery\": \"Môžeš byť objevený:\",\n        \"on-this-network\": \"v tejto sieti\",\n        \"on-this-network_title\": \"Ste viditeľný pre kohokoľvek v tejto sieti.\",\n        \"paired-devices_title\": \"Pre spárované zariadenia ste viditeľný vždy, nezávisle od siete.\",\n        \"public-room-devices\": \"v miestnosti {{roomId}}\",\n        \"paired-devices\": \"pomocou spárovaných zariadení\",\n        \"public-room-devices_title\": \"Môžeš byť objavený zariadeniami v tejto verejnej miestnosti. Nezávisle od siete, ku ktorej si pripojený.\",\n        \"traffic\": \"Premávka je\",\n        \"routed\": \"presmerovaný cez server\",\n        \"webrtc\": \"ak WebRTC nie je k dispozícii.\"\n    },\n    \"dialogs\": {\n        \"base64-title-text\": \"Zdielať text\",\n        \"base64-text\": \"text\",\n        \"send\": \"Odoslať\",\n        \"public-room-qr-code_title\": \"Kliknutím skopíruješ odkaz do verejnej miestnosti\",\n        \"pair-devices-title\": \"Spáruj zariadenia natrvalo\",\n        \"temporary-public-room-title\": \"Dočasná verejná miestnosť\",\n        \"input-key-on-this-device\": \"Zadaj tento kľúč na druhom zariadení\",\n        \"input-room-id-on-another-device\": \"Zadaj toto ID miestnosti na druhom zariadení\",\n        \"scan-qr-code\": \"alebo naskenuj QR kód.\",\n        \"enter-key-from-another-device\": \"Vlož kľúč z druhého zariadenia.\",\n        \"enter-room-id-from-another-device\": \"Na pripojenie k miestnosti, zadaj ID miestnosti druhého zariadenia.\",\n        \"hr-or\": \"ALEBO\",\n        \"pair\": \"Spárovať\",\n        \"cancel\": \"Zrušiť\",\n        \"edit-paired-devices-title\": \"Upraviť spárované zariadenia\",\n        \"unpair\": \"Zrušiť spárovanie\",\n        \"paired-device-removed\": \"Spárované zariadenie bolo odstránené.\",\n        \"paired-devices-wrapper_data-empty\": \"Žiadne spárované zariadenie.\",\n        \"auto-accept-instructions-1\": \"Aktivovať\",\n        \"auto-accept\": \"Automaticky prijať\",\n        \"auto-accept-instructions-2\": \"Automaticky príjmeš všetky súbory odoslané z tohto zariadenia.\",\n        \"close\": \"Zavrieť\",\n        \"send-message-title\": \"Poslať správu\",\n        \"send-message-to\": \"Komu:\",\n        \"join\": \"Pripojiť\",\n        \"leave\": \"Odpojiť\",\n        \"would-like-to-share\": \"by rád zdieľal\",\n        \"accept\": \"Prijať\",\n        \"decline\": \"Odmietnuť\",\n        \"has-sent\": \"odoslal:\",\n        \"share\": \"Zdielať\",\n        \"download\": \"Stiahnuť\",\n        \"message_placeholder\": \"Text\",\n        \"message_title\": \"Sem vlož správu\",\n        \"copy\": \"Kopírovať\",\n        \"base64-title-files\": \"Zdielať súbory\",\n        \"base64-processing\": \"Pracujem…\",\n        \"base64-tap-to-paste\": \"Ťuknutím sem zdieľaj {{type}}\",\n        \"base64-paste-to-send\": \"Tu vlož obsah schránky, ak ho chceš zdielať {{type}}\",\n        \"base64-files\": \"súbory\",\n        \"file-other-description-image\": \"a 1 ďalší obrázok\",\n        \"file-other-description-file\": \"a 1 ďalší súbor\",\n        \"file-other-description-image-plural\": \"a {{count}} ďalších obrázkov\",\n        \"file-other-description-file-plural\": \"a {{count}} ďalších súborov\",\n        \"title-image\": \"Obrázok\",\n        \"title-file\": \"Súbor\",\n        \"title-image-plural\": \"Obrázky\",\n        \"title-file-plural\": \"Súbory\",\n        \"receive-title\": \"{{descriptor}} Prijaté\",\n        \"download-again\": \"Stiahnuť znova\",\n        \"language-selector-title\": \"Nastaviť jazyk\",\n        \"system-language\": \"Jazyk systému\",\n        \"pair-devices-qr-code_title\": \"Kliknutím skopíruješ odkaz pre spárovanie tohto zariadenia\",\n        \"approve\": \"schváliť\",\n        \"share-text-title\": \"Zdielať textovú správu\",\n        \"share-text-subtitle\": \"Upraviť správu pred odoslaním:\",\n        \"share-text-checkbox\": \"Pri zdieľaní textu vždy zobraziť tento dialóg\",\n        \"close-toast_title\": \"Zavrieť oznámenie\",\n        \"receive-text-title\": \"Správa prijatá\"\n    },\n    \"notifications\": {\n        \"text-content-incorrect\": \"Obsah textu je chybný\",\n        \"clipboard-content-incorrect\": \"Obsah schránky je chybný\",\n        \"display-name-random-again\": \"Zobrazované meno je opäť generované náhodne\",\n        \"pairing-key-invalid\": \"Neplatný kľúč\",\n        \"pair-url-copied-to-clipboard\": \"Odkaz pre spárovanie tohto zariadenia bol skopírovaný do schránky\",\n        \"display-name-changed-permanently\": \"Zobrazované meno je trvalo zmenené\",\n        \"display-name-changed-temporarily\": \"Zobrazované meno je zmenené len pre túto reláciu\",\n        \"download-successful\": \"{{descriptor}} stiahnuté\",\n        \"file-content-incorrect\": \"Obsah súboru je chybný\",\n        \"pairing-tabs-error\": \"Spárovanie dvoch kariet web prehliadača nie je možné\",\n        \"pairing-success\": \"Zariadenia spárované\",\n        \"pairing-not-persistent\": \"Spárované zariadenie nie sú trvalé\",\n        \"pairing-key-invalidated\": \"Kľúč {{key}} už je neplatný\",\n        \"pairing-cleared\": \"Všetky zariadenia sú nespárované\",\n        \"public-room-id-invalid\": \"Chybné ID miestnosti\",\n        \"public-room-left\": \"Verejná miestnosť {{publicRoomId}} opustená\",\n        \"copied-to-clipboard\": \"Skopírované do schránky\",\n        \"room-url-copied-to-clipboard\": \"Odkaz do verejnej miestnosti bol skopírovaný do schránky\",\n        \"copied-to-clipboard-error\": \"Kopírovanie nie je možné. Skopíruj ručne.\",\n        \"notifications-enabled\": \"Oznámenia zapnuté\",\n        \"click-to-download\": \"Kliknutím stiahneš\",\n        \"request-title\": \"{{name}} chce poslať {{count}} {{descriptor}}\",\n        \"click-to-show\": \"Kliknutím zobrazíš\",\n        \"copied-text\": \"Text bol skopírovaný do schránky\",\n        \"link-received\": \"Odkaz prijatý od {{name}} – kliknutím otvoríš\",\n        \"message-received\": \"Správa prijatá od {{name}} – kliknutím skopíruješ\",\n        \"copied-text-error\": \"Kopírovanie do schránky zlyhalo. Skopíruj ručne!\",\n        \"offline\": \"Si offline\",\n        \"online\": \"Opäť si online\",\n        \"connected\": \"Pripojené\",\n        \"online-requirement-pairing\": \"Pre párovanie zariadení musíš byť online\",\n        \"online-requirement-public-room\": \"Pre vytvorenie miestnosti musíš byť online\",\n        \"connecting\": \"Pripájam…\",\n        \"files-incorrect\": \"Súbory sú chybné\",\n        \"file-transfer-completed\": \"Prenos súboru dokončený\",\n        \"ios-memory-limit\": \"Odosielanie súborov do iOS je možné len do veľkosti 200 MB\",\n        \"message-transfer-completed\": \"Správa odoslaná\",\n        \"unfinished-transfers-warning\": \"Máš nedokončené prenosy. Naozaj chceš zavrieť PairDrop?\",\n        \"rate-limit-join-key\": \"Dosiahol si limit. Počkaj 10 sekúnd a skús znovu.\",\n        \"selected-peer-left\": \"Vybraný užívateľ sa odpojil\",\n        \"notifications-permissions-error\": \"Notifikačné oprávnenie bolo zablokované, keďže užívateľ niekoľkokrát odmietol notifikačnú výzvu. Toto sa dá resetovat v nastavení webu, po kliknutí na ikonu zámku pri URL paneli.\"\n    },\n    \"about\": {\n        \"github_title\": \"PairDrop na GitHube\",\n        \"claim\": \"Jednoduchý spôsob posielania súborov medzi zariadeniami\",\n        \"buy-me-a-coffee_title\": \"Kúp mi kávičku!\",\n        \"tweet_title\": \"Tweetuj o PairDrop\",\n        \"mastodon_title\": \"Napíš o PairDrop na Mastodon\",\n        \"bluesky_title\": \"Sleduj nás na BlueSky\",\n        \"custom_title\": \"Sleduj nás\",\n        \"privacypolicy_title\": \"Naše zásady o ochrane súkromia\",\n        \"faq_title\": \"Často kladené otázky\",\n        \"close-about_aria-label\": \"Zatvoriť \\\"O službe PairDrop\\\"\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Súbor prijatý\",\n        \"file-received-plural\": \"Počet prijatých súborov: {{count}}\",\n        \"file-transfer-requested\": \"Prenos súboru vyžiadaný\",\n        \"image-transfer-requested\": \"Prenos obrázku vyžiadaný\",\n        \"message-received\": \"Správa prijatá\",\n        \"message-received-plural\": \"Počet prijatých správ: {{count}}\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Kliknutím odošleš {{descriptor}}\",\n        \"click-to-send\": \"Kliknutím odošleš súbory alebo pravým kliknutím odošleš správu\",\n        \"connection-hash\": \"Na overenie bezpečnosti end-to-end šifrovania, porovnaj toto číslo na oboch zariadeniach\",\n        \"preparing\": \"Pripravujem…\",\n        \"waiting\": \"Čakám…\",\n        \"processing\": \"Pracujem…\",\n        \"transferring\": \"Prenášam…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/ta.json",
    "content": "{\n    \"header\": {\n        \"language-selector_title\": \"மொழியை அமைக்கவும்\",\n        \"about_aria-label\": \"இணை ட்ராப் பற்றி திறந்திருக்கும்\",\n        \"theme-auto_title\": \"தானாகவே கருப்பொருள் கணினிக்கு மாற்றியமைக்கவும்\",\n        \"theme-light_title\": \"எப்போதும் ஒளி கருப்பொருளைப் பயன்படுத்துங்கள்\",\n        \"theme-dark_title\": \"எப்போதும் இருண்ட கருப்பொருளைப் பயன்படுத்துங்கள்\",\n        \"notification_title\": \"அறிவிப்புகளை இயக்கவும்\",\n        \"install_title\": \"இணை டிராப்பை நிறுவவும்\",\n        \"pair-device_title\": \"உங்கள் சாதனங்களை நிரந்தரமாக இணைக்கவும்\",\n        \"edit-paired-devices_title\": \"இணை சாதனங்களைத் திருத்தவும்\",\n        \"join-public-room_title\": \"தற்காலிகமாக பொது அறையில் சேரவும்\",\n        \"cancel-share-mode\": \"ரத்துசெய்\",\n        \"about_title\": \"இணை டிராப் பற்றி\",\n        \"edit-share-mode\": \"தொகு\",\n        \"expand_title\": \"தலைப்பு பொத்தான் வரிசையை விரிவாக்குங்கள்\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"பெறுநரைத் தேர்ந்தெடுக்க வெளியீடு\",\n        \"no-peers-title\": \"கோப்புகளை அனுப்ப பிற சாதனங்களில் இணை டிராப்பைத் திறக்கவும்\",\n        \"no-peers-subtitle\": \"சாதனங்களை இணை செய்யுங்கள் அல்லது பிற நெட்வொர்க்குகளில் கண்டறிய ஒரு பொது அறையை உள்ளிடவும்\",\n        \"x-instructions_desktop\": \"கோப்புகளை அனுப்ப சொடுக்கு செய்யவும் அல்லது செய்தியை அனுப்ப வலது சொடுக்கு செய்யவும்\",\n        \"x-instructions_mobile\": \"கோப்புகளை அனுப்ப தட்டவும் அல்லது செய்தியை அனுப்ப நீண்ட தட்டவும்\",\n        \"x-instructions_data-drop-peer\": \"பியருக்கு அனுப்ப வெளியீடு\",\n        \"x-instructions_data-drop-bg\": \"பெறுநரைத் தேர்ந்தெடுக்க வெளியீடு\",\n        \"x-instructions-share-mode_desktop\": \"{{descriptor}} ஐ அனுப்ப சொடுக்கு செய்க\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} பகிரப்பட்ட கோப்புகள்\",\n        \"x-instructions-share-mode_mobile\": \"{{descriptor}} அனுப்ப தட்டவும்\",\n        \"activate-share-mode-shared-file\": \"பகிரப்பட்ட கோப்பு\",\n        \"activate-share-mode-base\": \"அனுப்ப மற்ற சாதனங்களில் இணைக்கவும்\",\n        \"activate-share-mode-and-other-file\": \"மற்றும் 1 பிற கோப்பு\",\n        \"activate-share-mode-and-other-files-plural\": \"மற்றும் {{count}} பிற கோப்புகள்\",\n        \"activate-share-mode-shared-text\": \"பகிரப்பட்ட உரை\",\n        \"webrtc-requirement\": \"இந்த இணை டிராப் நிகழ்வைப் பயன்படுத்த, WEBRTC இயக்கப்பட்டிருக்க வேண்டும்!\"\n    },\n    \"footer\": {\n        \"display-name_data-placeholder\": \"ஏற்றுகிறது…\",\n        \"display-name_title\": \"உங்கள் சாதனத்தின் பெயரை நிரந்தரமாக திருத்தவும்\",\n        \"discovery\": \"நீங்கள் கண்டுபிடிக்கலாம்:\",\n        \"on-this-network\": \"இந்த நெட்வொர்க்கில்\",\n        \"on-this-network_title\": \"இந்த நெட்வொர்க்கில் உள்ள அனைவராலும் நீங்கள் கண்டுபிடிக்க முடியும்.\",\n        \"paired-devices\": \"இணை சாதனங்கள் மூலம்\",\n        \"paired-devices_title\": \"நெட்வொர்க்கிலிருந்து சுயாதீனமாக எல்லா நேரங்களிலும் இணை சாதனங்களால் நீங்கள் கண்டுபிடிக்கலாம்.\",\n        \"public-room-devices\": \"அறையில் {{roomId}}\",\n        \"public-room-devices_title\": \"நெட்வொர்க்கிலிருந்து சுயாதீனமான இந்த பொது அறையில் உள்ள சாதனங்களால் நீங்கள் கண்டுபிடிக்கலாம்.\",\n        \"traffic\": \"போக்குவரத்து\",\n        \"routed\": \"சேவையகம் வழியாக திசைதிருப்பப்பட்டது\",\n        \"webrtc\": \"WEBRTC கிடைக்கவில்லை என்றால்.\",\n        \"known-as\": \"நீங்கள் அறியப்படுகிறீர்கள்:\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"இணை சாதனங்கள் நிரந்தரமாக\",\n        \"input-key-on-this-device\": \"இந்த விசையை மற்றொரு சாதனத்தில் உள்ளிடவும்\",\n        \"scan-qr-code\": \"அல்லது QR-குறியீட்டை ச்கேன் செய்யுங்கள்.\",\n        \"enter-key-from-another-device\": \"மற்றொரு சாதனத்திலிருந்து விசையை இங்கே உள்ளிடவும்.\",\n        \"temporary-public-room-title\": \"தற்காலிக பொது அறை\",\n        \"input-room-id-on-another-device\": \"இந்த அறை ஐடியை மற்றொரு சாதனத்தில் உள்ளிடவும்\",\n        \"enter-room-id-from-another-device\": \"அறையில் சேர மற்றொரு சாதனத்திலிருந்து அறை ஐடியை உள்ளிடவும்.\",\n        \"hr-or\": \"அல்லது\",\n        \"pair\": \"பேரிக்காய்\",\n        \"cancel\": \"ரத்துசெய்\",\n        \"edit-paired-devices-title\": \"இணை சாதனங்களைத் திருத்தவும்\",\n        \"unpair\": \"அவிழ்த்து விடுங்கள்\",\n        \"paired-device-removed\": \"இணை சாதனம் அகற்றப்பட்டது.\",\n        \"paired-devices-wrapper_data-empty\": \"இணை சாதனங்கள் இல்லை.\",\n        \"auto-accept-instructions-1\": \"செயல்படுத்து\",\n        \"auto-accept\": \"தானாக ஏற்றுக்கொள்ளுங்கள்\",\n        \"auto-accept-instructions-2\": \"அந்த சாதனத்திலிருந்து அனுப்பப்பட்ட அனைத்து கோப்புகளையும் தானாக ஏற்றுக்கொள்ள.\",\n        \"close\": \"மூடு\",\n        \"join\": \"சேர\",\n        \"leave\": \"விடுப்பு\",\n        \"would-like-to-share\": \"பகிர்ந்து கொள்ள விரும்புகிறேன்\",\n        \"accept\": \"ஏற்றுக்கொள்\",\n        \"decline\": \"வீழ்ச்சி\",\n        \"has-sent\": \"அனுப்பியுள்ளது:\",\n        \"share\": \"பங்கு\",\n        \"download\": \"பதிவிறக்கம்\",\n        \"send-message-title\": \"செய்தி அனுப்பவும்\",\n        \"send-message-to\": \"இதற்கு:\",\n        \"message_title\": \"அனுப்ப செய்தியைச் செருகவும்\",\n        \"message_placeholder\": \"உரை\",\n        \"send\": \"அனுப்பு\",\n        \"receive-text-title\": \"செய்தி பெறப்பட்டது\",\n        \"copy\": \"நகலெடு\",\n        \"base64-title-files\": \"கோப்புகளைப் பகிரவும்\",\n        \"base64-title-text\": \"உரையைப் பகிரவும்\",\n        \"base64-processing\": \"செயலாக்கம்…\",\n        \"base64-tap-to-paste\": \"{{type}} பகிர இங்கே தட்டவும்\",\n        \"base64-paste-to-send\": \"{{type}} ஐப் பகிர இங்கே கிளிப்போர்டை ஒட்டவும்\",\n        \"base64-text\": \"உரை\",\n        \"base64-files\": \"கோப்புகள்\",\n        \"file-other-description-image\": \"மற்றும் 1 பிற படம்\",\n        \"file-other-description-file\": \"மற்றும் 1 பிற கோப்பு\",\n        \"file-other-description-image-plural\": \"மற்றும் {{count}} பிற படங்கள்\",\n        \"file-other-description-file-plural\": \"மற்றும் {{count}} பிற கோப்புகள்\",\n        \"title-image\": \"படம்\",\n        \"title-file\": \"கோப்பு\",\n        \"title-image-plural\": \"படங்கள்\",\n        \"title-file-plural\": \"கோப்புகள்\",\n        \"receive-title\": \"{{descriptor}} பெறப்பட்டது\",\n        \"download-again\": \"மீண்டும் பதிவிறக்கவும்\",\n        \"language-selector-title\": \"மொழியை அமைக்கவும்\",\n        \"system-language\": \"கணினி மொழி\",\n        \"public-room-qr-code_title\": \"பொது அறைக்கு இணைப்பை நகலெடுக்க சொடுக்கு செய்க\",\n        \"pair-devices-qr-code_title\": \"இந்த சாதனத்தை இணைக்க இணைப்பை நகலெடுக்க சொடுக்கு செய்க\",\n        \"approve\": \"ஒப்புதல்\",\n        \"share-text-title\": \"உரை செய்தியைப் பகிரவும்\",\n        \"share-text-subtitle\": \"அனுப்புவதற்கு முன் செய்தியைத் திருத்தவும்:\",\n        \"share-text-checkbox\": \"உரையைப் பகிரும்போது எப்போதும் இந்த உரையாடலைக் காட்டுங்கள்\",\n        \"close-toast_title\": \"அறிவிப்பை மூடு\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"இணை ட்ராப் பற்றி மூடு\",\n        \"claim\": \"சாதனங்களில் கோப்புகளை மாற்ற எளிதான வழி\",\n        \"github_title\": \"கிட்அப்பில் இணை டிராப்\",\n        \"buy-me-a-coffee_title\": \"எனக்கு ஒரு காபி வாங்க!\",\n        \"tweet_title\": \"இணை ட்ராப் பற்றி ட்வீட் செய்யுங்கள்\",\n        \"mastodon_title\": \"மாச்டோடனில் இணை ட்ராப் பற்றி எழுதுங்கள்\",\n        \"bluesky_title\": \"ப்ளூச்கியில் எங்களைப் பின்தொடரவும்\",\n        \"custom_title\": \"எங்களைப் பின்தொடரவும்\",\n        \"privacypolicy_title\": \"எங்கள் தனியுரிமைக் கொள்கையைத் திறக்கவும்\",\n        \"faq_title\": \"அடிக்கடி கேட்கப்படும் கேள்விகள்\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"காட்சி பெயர் நிரந்தரமாக மாற்றப்பட்டுள்ளது\",\n        \"display-name-changed-temporarily\": \"இந்த அமர்வுக்கு மட்டுமே காட்சி பெயர் மாற்றப்பட்டுள்ளது\",\n        \"display-name-random-again\": \"காட்சி பெயர் தோராயமாக மீண்டும் உருவாக்கப்படுகிறது\",\n        \"download-successful\": \"{{descriptor}} பதிவிறக்கம் செய்யப்பட்டது\",\n        \"pairing-tabs-error\": \"இரண்டு வலை உலாவி தாவல்களை இணைப்பது சாத்தியமற்றது\",\n        \"pairing-success\": \"சாதனங்கள் இணை\",\n        \"pairing-cleared\": \"அனைத்து சாதனங்களும் இணைக்கப்படவில்லை\",\n        \"public-room-id-invalid\": \"தவறான அறை ஐடி\",\n        \"public-room-left\": \"இடது பொது அறை {{publicRoomId}}}\",\n        \"text-content-incorrect\": \"உரை உள்ளடக்கம் தவறானது\",\n        \"file-content-incorrect\": \"கோப்பு உள்ளடக்கம் தவறானது\",\n        \"clipboard-content-incorrect\": \"இடைநிலைப்பலகை உள்ளடக்கம் தவறானது\",\n        \"notifications-enabled\": \"அறிவிப்புகள் இயக்கப்பட்டன\",\n        \"notifications-permissions-error\": \"பயனர் இசைவு வரியை பல முறை நிராகரித்ததால் அறிவிப்புகள் இசைவு தடுக்கப்பட்டுள்ளது. முகவரி பட்டியின் அடுத்த பூட்டு ஐகானைக் சொடுக்கு செய்வதன் மூலம் அணுகக்கூடிய பக்கத் தகவலில் இதை மீட்டமைக்க முடியும்.\",\n        \"link-received\": \"இணைப்பு {{name}} - திறக்க சொடுக்கு செய்க\",\n        \"message-received\": \"{{name} by ஆல் பெறப்பட்ட செய்தி - நகலெடுக்க சொடுக்கு செய்க\",\n        \"request-title\": \"{{name}} {{count}} {{descriptor}} ஐ மாற்ற விரும்புகிறது\",\n        \"click-to-show\": \"காண்பிக்க சொடுக்கு செய்க\",\n        \"copied-text\": \"இடைநிலைப்பலகைக்கு உரையை நகலெடுத்தது\",\n        \"copied-text-error\": \"இடைநிலைப்பலகைக்கு எழுதுவது தோல்வியடைந்தது. கைமுறையாக நகலெடுக்கவும்!\",\n        \"offline\": \"நீங்கள் ஆஃப்லைனில் இருக்கிறீர்கள்\",\n        \"online\": \"நீங்கள் ஆன்லைனில் திரும்பி வந்துள்ளீர்கள்\",\n        \"connected\": \"இணைக்கப்பட்டுள்ளது\",\n        \"online-requirement-pairing\": \"இணை சாதனங்களுக்கு நீங்கள் ஆன்லைனில் இருக்க வேண்டும்\",\n        \"online-requirement-public-room\": \"ஒரு பொது அறையை உருவாக்க நீங்கள் ஆன்லைனில் இருக்க வேண்டும்\",\n        \"connecting\": \"இணைத்தல்…\",\n        \"files-incorrect\": \"கோப்புகள் தவறானவை\",\n        \"file-transfer-completed\": \"கோப்பு பரிமாற்றம் முடிந்தது\",\n        \"ios-memory-limit\": \"ஐஇமு க்கு கோப்புகளை அனுப்புவது ஒரே நேரத்தில் 200 எம்பி வரை மட்டுமே சாத்தியமாகும்\",\n        \"unfinished-transfers-warning\": \"முடிக்கப்படாத இடமாற்றங்கள் உள்ளன. நீங்கள் நிச்சயமாக இணை டிராப்பை மூட விரும்புகிறீர்களா?\",\n        \"selected-peer-left\": \"தேர்ந்தெடுக்கப்பட்ட பியர் இடது\",\n        \"pairing-not-persistent\": \"இணை சாதனங்கள் தொடர்ந்து இல்லை\",\n        \"pairing-key-invalid\": \"தவறான விசை\",\n        \"pairing-key-invalidated\": \"விசை {{key}} செல்லாதது\",\n        \"copied-to-clipboard\": \"இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது\",\n        \"pair-url-copied-to-clipboard\": \"இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்ட இந்த சாதனத்தை இணைக்க இணைப்பு\",\n        \"room-url-copied-to-clipboard\": \"இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்ட பொது அறைக்கான இணைப்பு\",\n        \"copied-to-clipboard-error\": \"நகலெடுப்பது சாத்தியமில்லை. கைமுறையாக நகலெடுக்கவும்.\",\n        \"click-to-download\": \"பதிவிறக்க சொடுக்கு செய்க\",\n        \"message-transfer-completed\": \"செய்தி பரிமாற்றம் முடிந்தது\",\n        \"rate-limit-join-key\": \"வீத வரம்பு எட்டப்பட்டது. 10 வினாடிகள் காத்திருந்து மீண்டும் முயற்சிக்கவும்.\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"கோப்பு பெறப்பட்டது\",\n        \"file-received-plural\": \"{{count}} கோப்புகள் பெறப்பட்டன\",\n        \"image-transfer-requested\": \"பட பரிமாற்றம் கோரப்பட்டது\",\n        \"file-transfer-requested\": \"கோப்பு பரிமாற்றம் கோரப்பட்டது\",\n        \"message-received\": \"செய்தி பெறப்பட்டது\",\n        \"message-received-plural\": \"{{count}} செய்திகள் பெறப்பட்டன\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"{{descriptor}} ஐ அனுப்ப சொடுக்கு செய்க\",\n        \"click-to-send\": \"கோப்புகளை அனுப்ப சொடுக்கு செய்யவும் அல்லது செய்தியை அனுப்ப வலது சொடுக்கு செய்யவும்\",\n        \"connection-hash\": \"இறுதி முதல் இறுதி குறியாக்கத்தின் பாதுகாப்பை சரிபார்க்க, இந்த பாதுகாப்பு எண்ணை இரு சாதனங்களிலும் ஒப்பிடுக\",\n        \"waiting\": \"காத்திருக்கிறது…\",\n        \"processing\": \"செயலாக்கம்…\",\n        \"transferring\": \"இடமாற்றம்…\",\n        \"preparing\": \"தயாரித்தல்…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/th.json",
    "content": "{\n    \"footer\": {\n        \"display-name_title\": \"แก้ไขชื่ออุปกรณ์ของคุณอย่างถาวร\",\n        \"on-this-network_title\": \"ทุกคนในเครือข่ายนี้สามารถค้นพบคุณได้\",\n        \"paired-devices_title\": \"คุณสามารถค้นพบอุปกรณ์ที่จับคู่ได้ตลอดเวลาโดยไม่ขึ้นอยู่กับเครือข่าย\",\n        \"known-as\": \"ชื่ออุปกรณ์ของคุณ :\",\n        \"discovery\": \"คุณสามารถถูกค้นพบได้:\",\n        \"public-room-devices\": \"Room ID: {{roomId}}\",\n        \"public-room-devices_title\": \"คุณสามารถค้นพบอุปกรณ์ในห้องสาธารณะแห่งนี้ได้โดยไม่ขึ้นอยู่กับเครือข่าย\",\n        \"paired-devices\": \"โดยอุปกรณ์ที่จับคู่แล้ว\",\n        \"on-this-network\": \"บน Network เดียวกัน\",\n        \"display-name_data-placeholder\": \"โปรดรอสักครู่…\"\n    },\n    \"dialogs\": {\n        \"auto-accept-instructions-1\": \"เปิดใช้งาน\",\n        \"system-language\": \"ภาษาของระบบ\",\n        \"auto-accept-instructions-2\": \"เพื่อยอมรับไฟล์ทั้งหมดที่ส่งจากอุปกรณ์นั้นโดยอัตโนมัติ\",\n        \"copy\": \"คัดลอก\",\n        \"paired-device-removed\": \"อุปกรณ์ที่เคยจับคู่ถูกลบออกไปแล้ว\",\n        \"scan-qr-code\": \"หรือสแกน QR-code\",\n        \"cancel\": \"ยกเลิก\",\n        \"enter-room-id-from-another-device\": \"ป้อน Room ID เพื่อเข้าร่วม\",\n        \"pair\": \"จับคู่\",\n        \"hr-or\": \"หรือ\",\n        \"unpair\": \"ยกเลิกการจับคู่\",\n        \"auto-accept\": \"การยอมรับอัตโนมัติ\",\n        \"would-like-to-share\": \"อยากจะแบ่งปัน\",\n        \"accept\": \"ยอมรับ\",\n        \"decline\": \"ปฏิเสธ\",\n        \"message_placeholder\": \"ข้อความ\",\n        \"send-message-title\": \"ส่งข้อความ\",\n        \"send\": \"ส่ง\",\n        \"public-room-qr-code_title\": \"คลิกเพื่อคัดลอกลิงก์ไปยังห้องสาธารณะ\",\n        \"pair-devices-qr-code_title\": \"คลิกเพื่อคัดลอกลิงก์เพื่อจับคู่อุปกรณ์นี้\",\n        \"approve\": \"อนุมัติ\",\n        \"input-room-id-on-another-device\": \"ระบุ Room ID นี้บนอุปกรณ์อื่น\",\n        \"pair-devices-title\": \"การจับคู่อุปกรณ์\",\n        \"join\": \"เข้าร่วม\",\n        \"send-message-to\": \"ผู้รับ:\",\n        \"language-selector-title\": \"เลือกภาษา\",\n        \"close\": \"ปิด\",\n        \"leave\": \"ออกจากห้อง\",\n        \"temporary-public-room-title\": \"ห้องสาธารณะชั่วคราว\",\n        \"edit-paired-devices-title\": \"แก้ไขอุปกรณ์ที่จับคู่แล้ว\",\n        \"input-key-on-this-device\": \"ระบุ Key นี้บนอุปกรณ์อื่น\",\n        \"enter-key-from-another-device\": \"ระบุ Key ของอุปกรณ์อื่นที่นี่\"\n    },\n    \"header\": {\n        \"theme-auto_title\": \"ปรับธีมให้เข้ากับระบบโดยอัตโนมัติ\",\n        \"about_title\": \"เกี่ยวกับ PairDrop\",\n        \"theme-light_title\": \"ใช้ธีมสว่างเสมอ\",\n        \"theme-dark_title\": \"ใช้ธีมมืดเสมอ\",\n        \"notification_title\": \"เปิดใช้งานการแจ้งเตือน\",\n        \"install_title\": \"ติดตั้ง PairDrop\",\n        \"edit-paired-devices_title\": \"แก้ไขอุปกรณ์ที่จับคู่\",\n        \"cancel-share-mode\": \"ยกเลิก\",\n        \"join-public-room_title\": \"เข้าร่วมห้องสาธารณะชั่วคราว\",\n        \"edit-share-mode\": \"แก้ไข\",\n        \"pair-device_title\": \"การจับคู่อุปกรณ์\",\n        \"language-selector_title\": \"เลือกภาษา\"\n    },\n    \"notifications\": {\n        \"notifications-enabled\": \"เปิดใช้งานการแจ้งเตือนแล้ว\",\n        \"pairing-key-invalid\": \"Key ไม่ถูกต้อง\",\n        \"online\": \"คุณกลับมาออนไลน์แล้ว\",\n        \"display-name-changed-permanently\": \"ชื่อที่แสดงจะเปลี่ยนแปลงถาวร\",\n        \"display-name-changed-temporarily\": \"ชื่อที่แสดงมีการเปลี่ยนแปลงสำหรับเซสชันนี้เท่านั้น\",\n        \"display-name-random-again\": \"ชื่อที่แสดงจะถูกสร้างขึ้นแบบสุ่มอีกครั้ง\",\n        \"pairing-success\": \"จับคู่อุปกรณ์เรียบร้อยแล้ว\",\n        \"offline\": \"คุณออฟไลน์อยู่\"\n    },\n    \"about\": {\n        \"bluesky_title\": \"ติดตามเราได้ที่ BlueSky\",\n        \"claim\": \"วิธีที่ง่ายที่สุดในการถ่ายโอนไฟล์ระหว่างอุปกรณ์\",\n        \"github_title\": \"PairDrop บน GitHub\",\n        \"buy-me-a-coffee_title\": \"ซื้อกาแฟให้ฉันหน่อย! (บริจาค)\",\n        \"tweet_title\": \"ทวีตเกี่ยวกับ PairDrop\",\n        \"mastodon_title\": \"เขียนเกี่ยวกับ PairDrop บน Mastodon\",\n        \"custom_title\": \"ติดตามเรา\",\n        \"privacypolicy_title\": \"เปิดนโยบายความเป็นส่วนตัวของเรา\",\n        \"faq_title\": \"คำถามที่พบบ่อย (FAQ)\"\n    },\n    \"instructions\": {\n        \"no-peers-subtitle\": \"จับคู่อุปกรณ์ หรือ เข้าห้องสาธารณะ เพื่อ ค้นหาอุปกรณ์ ภายนอก เครือข่าย\",\n        \"x-instructions_mobile\": \"แตะเพื่อส่งข้อความ หรือ กดค้างไว้เพื่อส่งข้อความ\",\n        \"x-instructions_data-drop-peer\": \"ปล่อยเพื่อส่งให้อุปกรณ์\",\n        \"x-instructions-share-mode_desktop\": \"กดเพื่อส่ง{{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"แตะเพื่อส่ง{{descriptor}}\",\n        \"activate-share-mode-and-other-file\": \"และอีก 1 ไฟล์\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}}ไฟล์ที่แชร์\",\n        \"activate-share-mode-and-other-files-plural\": \"และ{{count}}ไฟล์ อื่นๆ\",\n        \"no-peers-title\": \"เปิด PairDrop บนอุปกรณ์อื่นเพื่อส่งไฟล์\",\n        \"x-instructions_desktop\": \"กดเพื่อส่งไฟล์ หรือ คลิ้กขวาเพื่อส่งไฟล์\"\n    }\n}\n"
  },
  {
    "path": "public/lang/tr.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"PairDrop Hakkında\",\n        \"about_aria-label\": \"PairDrop Hakkında'yı Aç\",\n        \"theme-auto_title\": \"Temayı sisteme otomatik olarak adapte et\",\n        \"theme-light_title\": \"Her zaman açık temayı kullan\",\n        \"theme-dark_title\": \"Her zaman koyu temayı kullan\",\n        \"notification_title\": \"Bildirimleri etkinleştir\",\n        \"install_title\": \"PairDrop'u Yükle\",\n        \"pair-device_title\": \"Cihazlarınızı kalıcı olarak eşleştirin\",\n        \"edit-paired-devices_title\": \"Eşleşmiş cihazları düzenle\",\n        \"cancel-share-mode\": \"İptal Et\",\n        \"join-public-room_title\": \"Geçici olarak ortak odaya katıl\",\n        \"language-selector_title\": \"Dili Ayarla\",\n        \"edit-share-mode\": \"Düzenle\",\n        \"expand_title\": \"Başlık buton satırını genişlet\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Alıcıyı seçmek için bırakın\",\n        \"x-instructions_mobile\": \"Dosya göndermek için dokunun veya mesaj göndermek için uzun dokunun\",\n        \"x-instructions-share-mode_desktop\": \"{{descriptor}} göndermek için tıklayın\",\n        \"activate-share-mode-and-other-files-plural\": \"ve {{count}} diğer dosya\",\n        \"x-instructions-share-mode_mobile\": \"{{descriptor}} göndermek için dokunun\",\n        \"activate-share-mode-base\": \"Göndermek için diğer cihazlarda PairDrop'u açın\",\n        \"no-peers-subtitle\": \"Cihazları eşleştirin veya keşfedilebilir olmak için ortak bir odaya girin\",\n        \"activate-share-mode-shared-text\": \"paylaşılan metin\",\n        \"x-instructions_desktop\": \"Dosya göndermek için tıklayın veya mesaj göndermek için sağ tıklayın\",\n        \"no-peers-title\": \"Dosya göndermek için diğer cihazlarda PairDrop'u açın\",\n        \"x-instructions_data-drop-peer\": \"Eşleştiriciye göndermek için bırakın\",\n        \"x-instructions_data-drop-bg\": \"Alıcıyı seçmek için bırakın\",\n        \"webrtc-requirement\": \"Bu PairDrop örneğini kullanmak için WebRTC etkin olmalı!\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} paylaşılan dosya\",\n        \"activate-share-mode-shared-file\": \"paylaşılan dosya\",\n        \"activate-share-mode-and-other-file\": \"ve 1 diğer dosya\"\n    },\n    \"footer\": {\n        \"display-name_data-placeholder\": \"Yükleniyor…\",\n        \"display-name_title\": \"Cihaz adınızı kalıcı olarak düzenleyin\",\n        \"webrtc\": \"WebRTC mevcut değilse.\",\n        \"public-room-devices_title\": \"Ağdan bağımsız olarak bu ortak odadaki cihazlar tarafından keşfedilebilir olabilirsiniz.\",\n        \"traffic\": \"Trafik\",\n        \"paired-devices_title\": \"Ağdan bağımsız olarak her zaman eşleştirilmiş cihazlar tarafından keşfedilebilir olabilirsiniz.\",\n        \"public-room-devices\": \"{{roomId}} odasında\",\n        \"paired-devices\": \"eşleştirilmiş cihazlar tarafından\",\n        \"on-this-network\": \"bu ağda\",\n        \"routed\": \"sunucu üzerinden yönlendirilmiş\",\n        \"discovery\": \"Keşfedilebilir durumdasınız:\",\n        \"on-this-network_title\": \"Bu ağdaki herkes tarafından keşfedilebilir olabilirsiniz.\",\n        \"known-as\": \"Şu adla biliniyorsunuz:\"\n    },\n    \"dialogs\": {\n        \"cancel\": \"İptal\",\n        \"edit-paired-devices-title\": \"Eşleşmiş Cihazları Düzenle\",\n        \"base64-paste-to-send\": \"{{type}} paylaşmak için buraya yapıştırın\",\n        \"auto-accept-instructions-2\": \"bu cihazdan gönderilen tüm dosyaları otomatik olarak kabul etmek için.\",\n        \"receive-text-title\": \"Mesaj Alındı\",\n        \"auto-accept-instructions-1\": \"Aktive et\",\n        \"pair-devices-title\": \"Cihazları Kalıcı Olarak Eşleştir\",\n        \"download\": \"İndir\",\n        \"title-file\": \"Dosya\",\n        \"base64-processing\": \"İşleniyor…\",\n        \"decline\": \"Reddet\",\n        \"receive-title\": \"{{descriptor}} Alındı\",\n        \"leave\": \"Ayrıl\",\n        \"message_title\": \"Göndermek için mesaj ekle\",\n        \"join\": \"Katıl\",\n        \"title-image-plural\": \"Resimler\",\n        \"send\": \"Gönder\",\n        \"base64-tap-to-paste\": \"{{type}} paylaşmak için buraya dokunun\",\n        \"base64-text\": \"metin\",\n        \"copy\": \"Kopyala\",\n        \"file-other-description-image\": \"ve 1 diğer resim\",\n        \"pair-devices-qr-code_title\": \"Bu cihazı eşleştirmek için bağlantıyı kopyalamak için tıklayın\",\n        \"temporary-public-room-title\": \"Geçici Ortak Oda\",\n        \"base64-files\": \"dosyalar\",\n        \"has-sent\": \"gönderdi:\",\n        \"file-other-description-file\": \"ve 1 diğer dosya\",\n        \"public-room-qr-code_title\": \"Ortak oda bağlantısını kopyalamak için tıklayın\",\n        \"close\": \"Kapat\",\n        \"system-language\": \"Sistem Dili\",\n        \"unpair\": \"Eşleşmeyi Kaldır\",\n        \"title-image\": \"Resim\",\n        \"file-other-description-file-plural\": \"ve {{count}} diğer dosya\",\n        \"would-like-to-share\": \"paylaşmak istiyor\",\n        \"send-message-to\": \"Alıcı:\",\n        \"language-selector-title\": \"Dili Ayarla\",\n        \"pair\": \"Eşleştir\",\n        \"hr-or\": \"VEYA\",\n        \"scan-qr-code\": \"veya QR kodunu tarayın.\",\n        \"input-key-on-this-device\": \"Bu anahtarı başka bir cihaza girin\",\n        \"download-again\": \"Tekrar indir\",\n        \"accept\": \"Kabul Et\",\n        \"paired-devices-wrapper_data-empty\": \"Eşleşmiş cihaz yok.\",\n        \"enter-key-from-another-device\": \"Başka bir cihazdan alınan anahtarı buraya girin.\",\n        \"share\": \"Paylaş\",\n        \"auto-accept\": \"otomatik kabul\",\n        \"title-file-plural\": \"Dosyalar\",\n        \"send-message-title\": \"Mesaj Gönder\",\n        \"input-room-id-on-another-device\": \"Bu oda kimliğini başka bir cihaza girin\",\n        \"file-other-description-image-plural\": \"ve {{count}} diğer resim\",\n        \"enter-room-id-from-another-device\": \"Odaya katılmak için başka bir cihazdan oda kimliğini girin.\",\n        \"message_placeholder\": \"Metin\",\n        \"close-toast_title\": \"Bildirim kapat\",\n        \"share-text-checkbox\": \"Metin paylaşırken her zaman bu iletişim kutusunu göster\",\n        \"base64-title-files\": \"Dosyaları Paylaş\",\n        \"approve\": \"onayla\",\n        \"paired-device-removed\": \"Eşleşmiş cihaz kaldırıldı.\",\n        \"share-text-title\": \"Metin Mesajı Paylaş\",\n        \"share-text-subtitle\": \"Göndermeden önce mesajı düzenleyin:\",\n        \"base64-title-text\": \"Metni Paylaş\"\n    },\n    \"notifications\": {\n        \"request-title\": \"{{name}} {{count}} {{descriptor}} transfer etmek istiyor\",\n        \"unfinished-transfers-warning\": \"Tamamlanmamış transferler var. PairDrop'u kapatmak istediğinizden emin misiniz?\",\n        \"message-received\": \"{{name}} tarafından mesaj alındı - Kopyalamak için tıklayın\",\n        \"notifications-permissions-error\": \"Bildirim izinleri birkaç kez reddedildiği için engellendi. Bu, URL çubuğunun yanındaki kilit simgesine tıklayarak erişilebilen Sayfa Bilgilerinde sıfırlanabilir.\",\n        \"rate-limit-join-key\": \"Hız sınırına ulaşıldı. 10 saniye bekleyin ve tekrar deneyin.\",\n        \"pair-url-copied-to-clipboard\": \"Bu cihazı eşleştirmek için bağlantı panoya kopyalandı\",\n        \"connecting\": \"Bağlanıyor…\",\n        \"pairing-key-invalidated\": \"Anahtar {{key}} geçersiz kılındı\",\n        \"pairing-key-invalid\": \"Geçersiz anahtar\",\n        \"connected\": \"Bağlı\",\n        \"pairing-not-persistent\": \"Eşleşmiş cihazlar kalıcı değil\",\n        \"text-content-incorrect\": \"Metin içeriği hatalı\",\n        \"message-transfer-completed\": \"Mesaj transferi tamamlandı\",\n        \"file-transfer-completed\": \"Dosya transferi tamamlandı\",\n        \"file-content-incorrect\": \"Dosya içeriği hatalı\",\n        \"files-incorrect\": \"Dosyalar hatalı\",\n        \"selected-peer-left\": \"Seçilen eş ayrıldı\",\n        \"link-received\": \"{{name}} tarafından bağlantı alındı - Açmak için tıklayın\",\n        \"online\": \"Tekrar çevrimiçisiniz\",\n        \"public-room-left\": \"Ortak odadan ayrıldı {{publicRoomId}}\",\n        \"copied-text\": \"Metin panoya kopyalandı\",\n        \"display-name-random-again\": \"Görünen ad tekrar rastgele oluşturuldu\",\n        \"display-name-changed-permanently\": \"Görünen ad kalıcı olarak değiştirildi\",\n        \"copied-to-clipboard-error\": \"Kopyalama mümkün değil. Elle kopyalayın.\",\n        \"pairing-success\": \"Cihazlar eşleştirildi\",\n        \"clipboard-content-incorrect\": \"Panodaki içerik hatalı\",\n        \"display-name-changed-temporarily\": \"Görünen ad sadece bu oturum için değiştirildi\",\n        \"copied-to-clipboard\": \"Panoya kopyalandı\",\n        \"offline\": \"Çevrimdışısınız\",\n        \"pairing-tabs-error\": \"İki web tarayıcı sekmesini eşleştirmek imkansız\",\n        \"public-room-id-invalid\": \"Geçersiz oda kimliği\",\n        \"click-to-download\": \"İndirmek için tıklayın\",\n        \"pairing-cleared\": \"Tüm cihazların eşleşmesi kaldırıldı\",\n        \"notifications-enabled\": \"Bildirimler etkinleştirildi\",\n        \"online-requirement-pairing\": \"Cihazları eşleştirmek için çevrimiçi olmalısınız\",\n        \"ios-memory-limit\": \"iOS'a dosya göndermek sadece bir seferde 200 MB'a kadar mümkündür\",\n        \"online-requirement-public-room\": \"Ortak oda oluşturmak için çevrimiçi olmalısınız\",\n        \"room-url-copied-to-clipboard\": \"Ortak oda bağlantısı panoya kopyalandı\",\n        \"copied-text-error\": \"Panoya yazma başarısız oldu. Elle kopyalayın!\",\n        \"download-successful\": \"{{descriptor}} indirildi\",\n        \"click-to-show\": \"Göstermek için tıklayın\"\n    },\n    \"peer-ui\": {\n        \"processing\": \"İşleniyor…\",\n        \"click-to-send-share-mode\": \"{{descriptor}} göndermek için tıklayın\",\n        \"click-to-send\": \"Dosya göndermek için tıklayın veya mesaj göndermek için sağ tıklayın\",\n        \"waiting\": \"Bekleniyor…\",\n        \"connection-hash\": \"Uçtan uca şifrelemenin güvenliğini doğrulamak için, bu güvenlik numarasını her iki cihazda da karşılaştırın\",\n        \"preparing\": \"Hazırlanıyor…\",\n        \"transferring\": \"Transfer ediliyor…\"\n    },\n    \"about\": {\n        \"claim\": \"Cihazlar arasında dosya aktarmanın en kolay yolu\",\n        \"tweet_title\": \"PairDrop hakkında tweet at\",\n        \"close-about_aria-label\": \"PairDrop Hakkında'yı Kapat\",\n        \"buy-me-a-coffee_title\": \"Bana bir kahve ısmarla!\",\n        \"github_title\": \"GitHub'da PairDrop\",\n        \"faq_title\": \"Sıkça sorulan sorular\",\n        \"custom_title\": \"Bizi takip edin\",\n        \"privacypolicy_title\": \"Gizlilik politikamızı aç\",\n        \"mastodon_title\": \"Mastodon'da PairDrop hakkında yaz\",\n        \"bluesky_title\": \"BlueSky'da bizi takip edin\"\n    },\n    \"document-titles\": {\n        \"file-transfer-requested\": \"Dosya Transferi Talep Edildi\",\n        \"image-transfer-requested\": \"Görüntü Transferi Talep Edildi\",\n        \"message-received-plural\": \"{{count}} Mesaj Alındı\",\n        \"message-received\": \"Mesaj Alındı\",\n        \"file-received\": \"Dosya Alındı\",\n        \"file-received-plural\": \"{{count}} Dosya Alındı\"\n    }\n}\n"
  },
  {
    "path": "public/lang/uk.json",
    "content": "{\n    \"header\": {\n        \"about_aria-label\": \"Відкрити \\\"Про PairDrop\\\"\",\n        \"theme-auto_title\": \"Автоматично адаптувати тему до системної\",\n        \"theme-light_title\": \"Завжди використовувати світлу тему\",\n        \"install_title\": \"Встановити PairDrop\",\n        \"join-public-room_title\": \"Приєднатися до публічної кімнати тимчасово\",\n        \"cancel-share-mode\": \"Скасувати\",\n        \"edit-share-mode\": \"Редагувати\",\n        \"about_title\": \"Про PairDrop\",\n        \"language-selector_title\": \"Встановити мову\",\n        \"theme-dark_title\": \"Завжди використовувати темну тему\",\n        \"pair-device_title\": \"Зв'язати ваші пристрої назавжди\",\n        \"notification_title\": \"Увімкнути сповіщення\",\n        \"edit-paired-devices_title\": \"Редагувати зв'язані пристрої\",\n        \"expand_title\": \"Розгорнути рядок кнопок заголовка\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"Відпустіть, щоб вибрати одержувача\",\n        \"x-instructions_desktop\": \"Натисніть, щоб надіслати файли, або клацніть правою кнопкою миші, щоб надіслати повідомлення\",\n        \"x-instructions_data-drop-peer\": \"Відпустіть, щоб надіслати партнеру\",\n        \"x-instructions-share-mode_desktop\": \"Натисніть, щоб надіслати {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"Торкніться, щоб надіслати {{descriptor}}\",\n        \"activate-share-mode-and-other-file\": \"та 1 інший файл\",\n        \"activate-share-mode-shared-file\": \"спільний файл\",\n        \"webrtc-requirement\": \"Щоб використовувати цей екземпляр PairDrop, WebRTC має бути увімкнено!\",\n        \"no-peers-title\": \"Відкрийте PairDrop на інших пристроях, щоб надіслати файли\",\n        \"no-peers-subtitle\": \"Зв’яжіть пристрої або введіть публічну кімнату, щоб бути помітним в інших мережах\",\n        \"x-instructions_mobile\": \"Торкніться, щоб надіслати файли, або довго натисніть, щоб надіслати повідомлення\",\n        \"x-instructions_data-drop-bg\": \"Відпустіть, щоб вибрати одержувача\",\n        \"activate-share-mode-base\": \"Відкрийте PairDrop на інших пристроях, щоб надіслати\",\n        \"activate-share-mode-and-other-files-plural\": \"та {{count}} інших файлів\",\n        \"activate-share-mode-shared-text\": \"спільний текст\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} спільних файлів\"\n    },\n    \"footer\": {\n        \"known-as\": \"Ви відомі як:\",\n        \"discovery\": \"Вас можна знайти:\",\n        \"public-room-devices\": \"у кімнаті {{roomId}}\",\n        \"public-room-devices_title\": \"Вас можуть знайти пристрої в цій публічній кімнаті, незалежно від мережі.\",\n        \"traffic\": \"Трафік\",\n        \"webrtc\": \"якщо WebRTC недоступний.\",\n        \"display-name_data-placeholder\": \"Завантаження…\",\n        \"display-name_title\": \"Редагувати назву вашого пристрою назавжди\",\n        \"on-this-network_title\": \"Вас можуть знайти всі на цій мережі.\",\n        \"routed\": \"маршрутизований через сервер\",\n        \"on-this-network\": \"в цій мережі\",\n        \"paired-devices\": \"через зв'язані пристрої\",\n        \"paired-devices_title\": \"Вас можуть знайти зв'язані пристрої в будь-який час, незалежно від мережі.\"\n    },\n    \"dialogs\": {\n        \"input-key-on-this-device\": \"Введіть цей ключ на іншому пристрої\",\n        \"scan-qr-code\": \"або відскануйте QR-код.\",\n        \"enter-key-from-another-device\": \"Введіть ключ з іншого пристрою тут.\",\n        \"temporary-public-room-title\": \"Тимчасова публічна кімната\",\n        \"input-room-id-on-another-device\": \"Введіть цей ID кімнати на іншому пристрої\",\n        \"enter-room-id-from-another-device\": \"Введіть ID кімнати з іншого пристрою, щоб приєднатися до кімнати.\",\n        \"hr-or\": \"АБО\",\n        \"cancel\": \"Скасувати\",\n        \"edit-paired-devices-title\": \"Редагувати Зв'язані пристрої\",\n        \"unpair\": \"Від'єднати\",\n        \"paired-device-removed\": \"Зв'язаний пристрій був видалений.\",\n        \"paired-devices-wrapper_data-empty\": \"Немає зв'язаних пристроїв.\",\n        \"auto-accept-instructions-1\": \"Активувати\",\n        \"auto-accept\": \"автоматичне прийняття\",\n        \"auto-accept-instructions-2\": \"щоб автоматично приймати всі файли, надіслані з цього пристрою.\",\n        \"join\": \"Приєднатися\",\n        \"leave\": \"Покинути\",\n        \"accept\": \"Прийняти\",\n        \"decline\": \"Відхилити\",\n        \"has-sent\": \"відправив:\",\n        \"share\": \"Поділитися\",\n        \"download\": \"Завантажити\",\n        \"send-message-title\": \"Надіслати повідомлення\",\n        \"send-message-to\": \"Кому:\",\n        \"message_title\": \"Введіть повідомлення для надсилання\",\n        \"base64-title-text\": \"Поділитися текстом\",\n        \"base64-processing\": \"Обробка…\",\n        \"base64-text\": \"текст\",\n        \"file-other-description-image\": \"та ще 1 зображення\",\n        \"file-other-description-file\": \"та ще 1 файл\",\n        \"file-other-description-image-plural\": \"та ще {{count}} зображень\",\n        \"title-file\": \"Файл\",\n        \"title-image-plural\": \"Зображення\",\n        \"title-file-plural\": \"Файли\",\n        \"receive-title\": \"{{descriptor}} отримано\",\n        \"system-language\": \"Системна мова\",\n        \"public-room-qr-code_title\": \"Натисніть, щоб скопіювати посилання на публічну кімнату\",\n        \"share-text-title\": \"Поділитися текстовим повідомленням\",\n        \"share-text-subtitle\": \"Редагувати повідомлення перед відправкою:\",\n        \"share-text-checkbox\": \"Завжди показувати цей діалог при поділі тексту\",\n        \"close-toast_title\": \"Закрити сповіщення\",\n        \"pair-devices-title\": \"Зв’язати пристрої назавжди\",\n        \"pair\": \"Приєднати\",\n        \"close\": \"Закрити\",\n        \"would-like-to-share\": \"хоче поділитися\",\n        \"copy\": \"Копіювати\",\n        \"message_placeholder\": \"Текст\",\n        \"send\": \"Надіслати\",\n        \"base64-title-files\": \"Поділитися файлами\",\n        \"receive-text-title\": \"Повідомлення отримано\",\n        \"base64-tap-to-paste\": \"Натисніть тут, щоб поділитися {{type}}\",\n        \"base64-paste-to-send\": \"Вставте буфер обміну тут, щоб поділитися {{type}}\",\n        \"file-other-description-file-plural\": \"та ще {{count}} файлів\",\n        \"base64-files\": \"файли\",\n        \"title-image\": \"Зображення\",\n        \"language-selector-title\": \"Встановити мову\",\n        \"approve\": \"схвалити\",\n        \"download-again\": \"Завантажити знову\",\n        \"pair-devices-qr-code_title\": \"Натисніть, щоб скопіювати посилання для зв'язування цього пристрою\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"Закрити \\\"Про PairDrop\\\"\",\n        \"github_title\": \"PairDrop на GitHub\",\n        \"buy-me-a-coffee_title\": \"Купи мені каву!\",\n        \"tweet_title\": \"Твіт про PairDrop\",\n        \"bluesky_title\": \"Підписуйтесь на нас у BlueSky\",\n        \"privacypolicy_title\": \"Відкрити нашу політику конфіденційності\",\n        \"faq_title\": \"Часто задавані питання\",\n        \"mastodon_title\": \"Напишіть про PairDrop на Mastodon\",\n        \"custom_title\": \"Підписуйтесь на нас\",\n        \"claim\": \"Найпростіший спосіб передачі файлів між пристроями\"\n    },\n    \"notifications\": {\n        \"display-name-changed-temporarily\": \"Відображуване ім'я було змінено тільки для цієї сесії\",\n        \"display-name-random-again\": \"Відображуване ім'я згенерувалося випадковим чином знову\",\n        \"download-successful\": \"{{descriptor}} завантажено\",\n        \"pairing-tabs-error\": \"Зв'язування двох вкладок браузера неможливе\",\n        \"pairing-success\": \"Пристрої зв'язані\",\n        \"pairing-not-persistent\": \"Зв'язані пристрої не є постійними\",\n        \"pairing-key-invalid\": \"Недійсний ключ\",\n        \"pairing-key-invalidated\": \"Ключ {{key}} недійсний\",\n        \"pairing-cleared\": \"Всі пристрої роз'єднані\",\n        \"public-room-id-invalid\": \"Недійсний ID кімнати\",\n        \"public-room-left\": \"Покинув публічну кімнату {{publicRoomId}}\",\n        \"copied-to-clipboard-error\": \"Копіювання неможливе. Скопіюйте вручну.\",\n        \"clipboard-content-incorrect\": \"Вміст буфера обміну неправильний\",\n        \"link-received\": \"Посилання отримано від {{name}} - Натисніть, щоб відкрити\",\n        \"message-received\": \"Повідомлення отримано від {{name}} - Натисніть, щоб скопіювати\",\n        \"click-to-download\": \"Натисніть, щоб завантажити\",\n        \"request-title\": \"{{name}} хоче передати {{count}} {{descriptor}}\",\n        \"click-to-show\": \"Натисніть, щоб показати\",\n        \"copied-text\": \"Текст скопійовано в буфер обміну\",\n        \"copied-text-error\": \"Запис у буфер обміну не вдався. Скопіюйте вручну!\",\n        \"offline\": \"Ви офлайн\",\n        \"online-requirement-pairing\": \"Вам потрібно бути онлайн, щоб зв'язати пристрої\",\n        \"online-requirement-public-room\": \"Вам потрібно бути онлайн, щоб створити публічну кімнату\",\n        \"connecting\": \"Підключення…\",\n        \"ios-memory-limit\": \"Відправка файлів на iOS можлива лише до 200 МБ за один раз\",\n        \"message-transfer-completed\": \"Передача повідомлення завершена\",\n        \"rate-limit-join-key\": \"Досягнуто ліміт швидкості. Зачекайте 10 секунд і спробуйте знову.\",\n        \"selected-peer-left\": \"Обраний пір залишив\",\n        \"files-incorrect\": \"Файли неправильні\",\n        \"display-name-changed-permanently\": \"Відображуване ім'я було змінено назавжди\",\n        \"notifications-permissions-error\": \"Дозвіл на сповіщення було заблоковано, оскільки користувач кілька разів відхилив запит на дозвіл. Це можна скинути в інформації про сторінку, до якої можна отримати доступ, натиснувши значок замка поруч з рядком URL.\",\n        \"copied-to-clipboard\": \"Скопійовано в буфер обміну\",\n        \"pair-url-copied-to-clipboard\": \"Посилання для зв'язування цього пристрою скопійовано в буфер обміну\",\n        \"room-url-copied-to-clipboard\": \"Посилання на публічну кімнату скопійовано в буфер обміну\",\n        \"text-content-incorrect\": \"Текстовий вміст неправильний\",\n        \"file-content-incorrect\": \"Вміст файлу неправильний\",\n        \"notifications-enabled\": \"Сповіщення увімкнені\",\n        \"connected\": \"Підключено\",\n        \"online\": \"Ви знову онлайн\",\n        \"file-transfer-completed\": \"Передача файлу завершена\",\n        \"unfinished-transfers-warning\": \"Є незавершені передачі. Ви впевнені, що хочете закрити PairDrop?\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"Файл отримано\",\n        \"file-received-plural\": \"Отримано {{count}} файлів\",\n        \"image-transfer-requested\": \"Запит на передачу зображення\",\n        \"message-received\": \"Повідомлення отримано\",\n        \"message-received-plural\": \"Отримано {{count}} повідомлень\",\n        \"file-transfer-requested\": \"Запит на передачу файлу\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"Натисніть, щоб відправити {{descriptor}}\",\n        \"connection-hash\": \"Щоб перевірити безпеку кінцевого шифрування, порівняйте цей номер безпеки на обох пристроях\",\n        \"processing\": \"Обробка…\",\n        \"click-to-send\": \"Натисніть, щоб відправити файли, або клацніть правою кнопкою миші, щоб відправити повідомлення\",\n        \"preparing\": \"Підготовка…\",\n        \"waiting\": \"Чекаю…\",\n        \"transferring\": \"Переводимо…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/zh-CN.json",
    "content": "{\n    \"header\": {\n        \"about_title\": \"关于 PairDrop\",\n        \"about_aria-label\": \"打开 关于 PairDrop\",\n        \"theme-light_title\": \"总是使用明亮主题\",\n        \"install_title\": \"安装 PairDrop\",\n        \"pair-device_title\": \"永久配对您的设备\",\n        \"theme-auto_title\": \"主题适应系统\",\n        \"theme-dark_title\": \"总是使用暗黑主题\",\n        \"notification_title\": \"开启通知\",\n        \"edit-paired-devices_title\": \"管理已配对设备\",\n        \"cancel-share-mode\": \"完成\",\n        \"join-public-room_title\": \"暂时加入公共房间\",\n        \"language-selector_title\": \"设置语言\",\n        \"edit-share-mode\": \"编辑\",\n        \"expand_title\": \"展开标题按钮行\"\n    },\n    \"instructions\": {\n        \"x-instructions_data-drop-peer\": \"释放以发送到此设备\",\n        \"no-peers_data-drop-bg\": \"释放来选择接收者\",\n        \"no-peers-subtitle\": \"配对新设备 或 加入一个公共房间 以便在其他网络上可见\",\n        \"no-peers-title\": \"在其他设备上打开 PairDrop 来发送文件\",\n        \"x-instructions_desktop\": \"点击以发送文件 或 右键来发送信息\",\n        \"x-instructions_mobile\": \"轻触以发送文件 或 长按来发送信息\",\n        \"x-instructions_data-drop-bg\": \"释放来选择接收者\",\n        \"x-instructions-share-mode_desktop\": \"单击发送 {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"轻触发送 {{descriptor}}\",\n        \"activate-share-mode-base\": \"在其他设备上打开 PairDrop 来发送\",\n        \"activate-share-mode-and-other-files-plural\": \"和 {{count}} 个其他的文件\",\n        \"activate-share-mode-shared-text\": \"分享文本\",\n        \"webrtc-requirement\": \"要使用此 PairDrop 示例。必须开启 WebRTC！\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} 个已分享的文件\",\n        \"activate-share-mode-shared-file\": \"分享的文件\",\n        \"activate-share-mode-and-other-file\": \"及其余 1 个文件\"\n    },\n    \"footer\": {\n        \"routed\": \"途径服务器\",\n        \"webrtc\": \"如果 WebRTC 不可用。\",\n        \"known-as\": \"你的名字是:\",\n        \"display-name_data-placeholder\": \"加载中…\",\n        \"display-name_title\": \"修改你的默认设备名\",\n        \"on-this-network\": \"在此网络上\",\n        \"paired-devices\": \"已配对的设备\",\n        \"traffic\": \"流量将\",\n        \"public-room-devices_title\": \"您可以被这个独立于网络的公共房间中的设备发现。\",\n        \"paired-devices_title\": \"您可以在任何时候被已配对的设备发现，而不依赖于网络。\",\n        \"public-room-devices\": \"在房间 {{roomId}} 中\",\n        \"discovery\": \"您可以被发现:\",\n        \"on-this-network_title\": \"您可以被这个网络上的每个人发现。\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"配对新设备（常驻）\",\n        \"input-key-on-this-device\": \"在另一个设备上输入这串数字\",\n        \"base64-text\": \"信息\",\n        \"enter-key-from-another-device\": \"在此处输入从另一个设备上获得的数字。\",\n        \"edit-paired-devices-title\": \"管理已配对的设备\",\n        \"pair\": \"配对\",\n        \"cancel\": \"取消\",\n        \"scan-qr-code\": \"或者 扫描二维码。\",\n        \"paired-devices-wrapper_data-empty\": \"无已配对设备。\",\n        \"auto-accept-instructions-1\": \"启用\",\n        \"auto-accept\": \"自动接收\",\n        \"decline\": \"拒绝\",\n        \"base64-processing\": \"处理中…\",\n        \"base64-tap-to-paste\": \"轻触此处分享 {{type}}\",\n        \"base64-paste-to-send\": \"将剪贴板粘贴到此处以分享 {{type}}\",\n        \"auto-accept-instructions-2\": \"以无需同意而自动接收从那个设备上发送的所有文件。\",\n        \"would-like-to-share\": \"想要分享\",\n        \"accept\": \"接收\",\n        \"close\": \"关闭\",\n        \"share\": \"分享\",\n        \"download\": \"保存\",\n        \"send\": \"发送\",\n        \"receive-text-title\": \"收到信息\",\n        \"copy\": \"复制\",\n        \"send-message-title\": \"发送信息\",\n        \"send-message-to\": \"发给：\",\n        \"has-sent\": \"发送了：\",\n        \"base64-files\": \"文件\",\n        \"file-other-description-file\": \"和 1 个其他的文件\",\n        \"file-other-description-image\": \"和 1 个其他的图片\",\n        \"file-other-description-image-plural\": \"和 {{count}} 个其他的图片\",\n        \"file-other-description-file-plural\": \"和 {{count}} 个其他的文件\",\n        \"title-image-plural\": \"图片\",\n        \"receive-title\": \"收到 {{descriptor}}\",\n        \"title-image\": \"图片\",\n        \"title-file\": \"文件\",\n        \"title-file-plural\": \"文件\",\n        \"download-again\": \"再次保存\",\n        \"system-language\": \"跟随系统语言\",\n        \"unpair\": \"取消配对\",\n        \"language-selector-title\": \"设置语言\",\n        \"hr-or\": \"或者\",\n        \"input-room-id-on-another-device\": \"在另一个设备上输入这串房间号\",\n        \"leave\": \"离开\",\n        \"join\": \"加入\",\n        \"temporary-public-room-title\": \"临时公共房间\",\n        \"enter-room-id-from-another-device\": \"在另一个设备上输入这串房间号来加入房间。\",\n        \"message_title\": \"插入要发送的消息\",\n        \"pair-devices-qr-code_title\": \"单击复制和此设备配对的链接\",\n        \"public-room-qr-code_title\": \"单击复制公共房间链接\",\n        \"message_placeholder\": \"文本\",\n        \"close-toast_title\": \"关闭通知\",\n        \"share-text-checkbox\": \"分享文本时总是显示此对话框\",\n        \"base64-title-files\": \"分享文件\",\n        \"approve\": \"批准\",\n        \"paired-device-removed\": \"已删除配对的设备。\",\n        \"share-text-title\": \"分享文本消息\",\n        \"share-text-subtitle\": \"发送前编辑消息：\",\n        \"base64-title-text\": \"分享文本\"\n    },\n    \"about\": {\n        \"faq_title\": \"常见问题\",\n        \"close-about_aria-label\": \"关闭 关于 PairDrop\",\n        \"github_title\": \"PairDrop 在 GitHub 上开源\",\n        \"claim\": \"最简单的跨设备传输方案\",\n        \"buy-me-a-coffee_title\": \"帮我买杯咖啡！\",\n        \"tweet_title\": \"关于 PairDrop 的推特\",\n        \"bluesky_title\": \"在 BlueSky 上关注\",\n        \"privacypolicy_title\": \"打开隐私政策\",\n        \"mastodon_title\": \"在 Maston 上推广 PairDrop\",\n        \"custom_title\": \"关注我们\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"展示的名字已经永久变更\",\n        \"display-name-changed-temporarily\": \"展示名字仅在此会话中变更\",\n        \"display-name-random-again\": \"展示的名字已再次随机生成\",\n        \"download-successful\": \"{{descriptor}} 已下载\",\n        \"pairing-tabs-error\": \"无法配对两个浏览器标签页\",\n        \"pairing-success\": \"设备已配对\",\n        \"pairing-not-persistent\": \"配对的设备不是持久的\",\n        \"pairing-key-invalid\": \"无效配对码\",\n        \"pairing-key-invalidated\": \"配对码 {{key}} 已失效\",\n        \"text-content-incorrect\": \"文本内容不正确\",\n        \"file-content-incorrect\": \"文件内容不正确\",\n        \"clipboard-content-incorrect\": \"剪贴板内容不正确\",\n        \"link-received\": \"收到来自 {{name}} 的链接 - 点击打开\",\n        \"message-received\": \"收到来自 {{name}} 的信息 - 点击复制\",\n        \"request-title\": \"{{name}} 想要发送 {{count}} 个 {{descriptor}}\",\n        \"click-to-show\": \"点击展示\",\n        \"copied-text\": \"复制到剪贴板\",\n        \"selected-peer-left\": \"选择的 peer 已离开\",\n        \"pairing-cleared\": \"所有设备已解除配对\",\n        \"copied-to-clipboard\": \"已复制到剪贴板\",\n        \"notifications-enabled\": \"通知已启用\",\n        \"copied-text-error\": \"写入剪贴板失败。请手动复制！\",\n        \"click-to-download\": \"点击以保存\",\n        \"unfinished-transfers-warning\": \"还有未完成的传输任务。你确定要关闭 PairDrop 吗？\",\n        \"message-transfer-completed\": \"信息传输已完成\",\n        \"offline\": \"你未连接到网络\",\n        \"online\": \"你已重新连接到网络\",\n        \"connected\": \"已连接\",\n        \"online-requirement\": \"你需要连接网络来配对新设备。\",\n        \"files-incorrect\": \"文件不正确\",\n        \"file-transfer-completed\": \"文件传输已完成\",\n        \"connecting\": \"连接中…\",\n        \"ios-memory-limit\": \"向 iOS 发送文件 一次最多只能发送 200 MB\",\n        \"rate-limit-join-key\": \"已达连接限制。请等待 10秒 后再试。\",\n        \"public-room-left\": \"已退出公共房间 {{publicRoomId}}\",\n        \"copied-to-clipboard-error\": \"无法复制。请手动复制。\",\n        \"public-room-id-invalid\": \"无效的房间号\",\n        \"online-requirement-pairing\": \"您需要连接到互联网才能配对设备\",\n        \"online-requirement-public-room\": \"您需要连接到互联网来创建公共房间\",\n        \"notifications-permissions-error\": \"因用户数次拒绝了权限授予提示，通知权限已被拦截。可以在“页面信息”中重置它，要访问“页面信息”请单击地址栏旁的挂锁图标。\",\n        \"pair-url-copied-to-clipboard\": \"已将和此设备配对的链接复制到剪贴板\",\n        \"room-url-copied-to-clipboard\": \"已将公共房间的链接复制到剪贴板\"\n    },\n    \"document-titles\": {\n        \"message-received\": \"收到信息\",\n        \"message-received-plural\": \"收到 {{count}} 条信息\",\n        \"file-transfer-requested\": \"文件传输请求\",\n        \"file-received-plural\": \"收到 {{count}} 个文件\",\n        \"file-received\": \"收到文件\",\n        \"image-transfer-requested\": \"图片传输请求\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"点击发送 {{descriptor}}\",\n        \"click-to-send\": \"点击以发送文件 或 右键来发送信息\",\n        \"connection-hash\": \"若要验证端到端加密的安全性，请在两个设备上比较此安全编号\",\n        \"preparing\": \"准备中…\",\n        \"waiting\": \"请等待…\",\n        \"transferring\": \"传输中…\",\n        \"processing\": \"处理中…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/zh-HK.json",
    "content": "{\n    \"header\": {\n        \"expand_title\": \"展開標題按鈕列\",\n        \"about_title\": \"關於 PairDrop\",\n        \"language-selector_title\": \"設定語言\",\n        \"about_aria-label\": \"開啟 關於 PairDrop\",\n        \"theme-auto_title\": \"主題跟隨系統設定\",\n        \"theme-light_title\": \"固定使用明亮主題\",\n        \"theme-dark_title\": \"固定使用深色主題\",\n        \"notification_title\": \"啟用通知\",\n        \"install_title\": \"安裝 PairDrop\",\n        \"pair-device_title\": \"永久配對您的裝置\",\n        \"edit-paired-devices_title\": \"管理已配對裝置\",\n        \"join-public-room_title\": \"暫時加入公共房間\",\n        \"cancel-share-mode\": \"完成\",\n        \"edit-share-mode\": \"編輯\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"放開以選擇接收者\",\n        \"no-peers-title\": \"喺其他裝置開啟 PairDrop 嚟傳送檔案\",\n        \"no-peers-subtitle\": \"配對新裝置 或 加入公共房間 以便喺其他網絡顯示\",\n        \"x-instructions_desktop\": \"按一下傳送檔案 或 右鍵傳送訊息\",\n        \"x-instructions_mobile\": \"點擊傳送檔案 或 長按傳送訊息\",\n        \"activate-share-mode-shared-file\": \"已分享檔案\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} 個已分享檔案\",\n        \"x-instructions_data-drop-peer\": \"放開即可傳送至呢部裝置\",\n        \"x-instructions_data-drop-bg\": \"放開以選擇接收者\",\n        \"x-instructions-share-mode_desktop\": \"單擊傳送 {{descriptor}}\",\n        \"x-instructions-share-mode_mobile\": \"點擊傳送 {{descriptor}}\",\n        \"activate-share-mode-base\": \"喺其他裝置開啟 PairDrop 嚟傳送\",\n        \"activate-share-mode-and-other-file\": \"及另外 1 個檔案\",\n        \"activate-share-mode-and-other-files-plural\": \"及另外 {{count}} 個檔案\",\n        \"activate-share-mode-shared-text\": \"分享文字\",\n        \"webrtc-requirement\": \"使用此 PairDrop 實例需要啟用 WebRTC！\"\n    },\n    \"footer\": {\n        \"display-name_title\": \"修改預設裝置名稱\",\n        \"discovery\": \"你可被發現於:\",\n        \"on-this-network\": \"喺呢個網絡\",\n        \"on-this-network_title\": \"你可以被同一個網絡嘅所有人發現。\",\n        \"paired-devices\": \"已配對裝置\",\n        \"paired-devices_title\": \"你隨時可以被已配對嘅裝置發現，無需依賴網絡。\",\n        \"public-room-devices\": \"喺房間 {{roomId}} 內\",\n        \"public-room-devices_title\": \"你可以被呢個跨網絡公共房間內嘅裝置發現。\",\n        \"traffic\": \"流量將會\",\n        \"routed\": \"經伺服器中轉\",\n        \"webrtc\": \"若 WebRTC 無法使用。\",\n        \"known-as\": \"你嘅名稱係:\",\n        \"display-name_data-placeholder\": \"載入中…\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"配對新裝置（常駐）\",\n        \"input-key-on-this-device\": \"喺另一部裝置輸入呢組數字\",\n        \"scan-qr-code\": \"或者 掃描二維碼。\",\n        \"temporary-public-room-title\": \"臨時公共房間\",\n        \"input-room-id-on-another-device\": \"喺另一部裝置輸入呢個房間號碼\",\n        \"enter-room-id-from-another-device\": \"喺另一部裝置輸入房間號碼加入。\",\n        \"hr-or\": \"或\",\n        \"pair\": \"配對\",\n        \"cancel\": \"取消\",\n        \"edit-paired-devices-title\": \"管理已配對裝置\",\n        \"unpair\": \"解除配對\",\n        \"paired-device-removed\": \"已移除配對裝置。\",\n        \"paired-devices-wrapper_data-empty\": \"未有已配對裝置。\",\n        \"auto-accept-instructions-1\": \"啟用\",\n        \"auto-accept\": \"自動接收\",\n        \"auto-accept-instructions-2\": \"即可自動接收該裝置傳送嘅所有檔案，無需確認。\",\n        \"decline\": \"拒絕\",\n        \"receive-text-title\": \"收到訊息\",\n        \"base64-title-files\": \"分享檔案\",\n        \"base64-title-text\": \"分享文字\",\n        \"base64-processing\": \"處理中…\",\n        \"base64-tap-to-paste\": \"點擊此處分享 {{type}}\",\n        \"base64-paste-to-send\": \"將剪貼簿內容貼上嚟分享 {{type}}\",\n        \"base64-text\": \"訊息\",\n        \"base64-files\": \"檔案\",\n        \"file-other-description-image\": \"及另外 1 張圖片\",\n        \"file-other-description-file\": \"及另外 1 個檔案\",\n        \"file-other-description-image-plural\": \"及另外 {{count}} 張圖片\",\n        \"file-other-description-file-plural\": \"及另外 {{count}} 個檔案\",\n        \"title-image\": \"圖片\",\n        \"title-file\": \"檔案\",\n        \"title-image-plural\": \"圖片\",\n        \"title-file-plural\": \"檔案\",\n        \"receive-title\": \"收到 {{descriptor}}\",\n        \"download-again\": \"再次儲存\",\n        \"language-selector-title\": \"設定語言\",\n        \"pair-devices-qr-code_title\": \"按一下複製裝置配對連結\",\n        \"approve\": \"批准\",\n        \"share-text-title\": \"分享文字訊息\",\n        \"share-text-subtitle\": \"傳送前編輯訊息：\",\n        \"share-text-checkbox\": \"分享文字時永遠顯示此視窗\",\n        \"system-language\": \"跟隨系統語言\",\n        \"public-room-qr-code_title\": \"按一下複製公共房間連結\",\n        \"close-toast_title\": \"關閉通知\",\n        \"enter-key-from-another-device\": \"喺度輸入另一部裝置嘅配對碼。\",\n        \"close\": \"關閉\",\n        \"join\": \"加入\",\n        \"leave\": \"離開\",\n        \"would-like-to-share\": \"想分享\",\n        \"accept\": \"接收\",\n        \"has-sent\": \"傳送咗：\",\n        \"share\": \"分享\",\n        \"download\": \"儲存\",\n        \"send-message-title\": \"傳送訊息\",\n        \"send-message-to\": \"傳送至：\",\n        \"message_title\": \"輸入要傳送嘅訊息\",\n        \"message_placeholder\": \"文字\",\n        \"send\": \"傳送\",\n        \"copy\": \"複製\"\n    },\n    \"about\": {\n        \"close-about_aria-label\": \"關閉 關於 PairDrop\",\n        \"claim\": \"最簡單嘅跨裝置傳輸方案\",\n        \"github_title\": \"PairDrop 開源於 GitHub\",\n        \"buy-me-a-coffee_title\": \"請我飲杯咖啡！\",\n        \"tweet_title\": \"關於 PairDrop 嘅推文\",\n        \"mastodon_title\": \"喺 Mastodon 推廣 PairDrop\",\n        \"bluesky_title\": \"喺 BlueSky 關注\",\n        \"custom_title\": \"關注我哋\",\n        \"privacypolicy_title\": \"開啟私隱政策\",\n        \"faq_title\": \"常見問題\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"顯示名稱已永久變更\",\n        \"display-name-changed-temporarily\": \"顯示名稱僅於今次連線有效\",\n        \"display-name-random-again\": \"顯示名稱已重新隨機生成\",\n        \"download-successful\": \"{{descriptor}} 已儲存\",\n        \"pairing-tabs-error\": \"無法配對兩個瀏覽器分頁\",\n        \"pairing-success\": \"裝置已成功配對\",\n        \"pairing-not-persistent\": \"配對裝置未持久保存\",\n        \"pairing-key-invalid\": \"無效配對碼\",\n        \"pairing-key-invalidated\": \"配對碼 {{key}} 已失效\",\n        \"pairing-cleared\": \"所有裝置配對已解除\",\n        \"public-room-id-invalid\": \"無效房間號碼\",\n        \"public-room-left\": \"已離開公共房間 {{publicRoomId}}\",\n        \"copied-to-clipboard\": \"已複製到剪貼簿\",\n        \"pair-url-copied-to-clipboard\": \"裝置配對連結已複製到剪貼簿\",\n        \"room-url-copied-to-clipboard\": \"公共房間連結已複製到剪貼簿\",\n        \"copied-to-clipboard-error\": \"無法複製，請手動操作。\",\n        \"text-content-incorrect\": \"文字內容不正確\",\n        \"file-content-incorrect\": \"檔案內容不正確\",\n        \"clipboard-content-incorrect\": \"剪貼簿內容不正確\",\n        \"notifications-enabled\": \"通知已啟用\",\n        \"notifications-permissions-error\": \"因多次拒絕權限，通知功能已被封鎖。可點擊網址列嘅鎖頭圖示重置權限。\",\n        \"link-received\": \"收到來自 {{name}} 嘅連結 - 點擊開啟\",\n        \"click-to-download\": \"點擊儲存\",\n        \"message-received\": \"收到來自 {{name}} 嘅訊息 - 點擊複製\",\n        \"request-title\": \"{{name}} 想傳送 {{count}} 個 {{descriptor}}\",\n        \"click-to-show\": \"點擊顯示\",\n        \"copied-text\": \"已複製到剪貼簿\",\n        \"copied-text-error\": \"寫入剪貼簿失敗，請手動複製！\",\n        \"selected-peer-left\": \"已選擇嘅接收者離開\",\n        \"unfinished-transfers-warning\": \"仍有未完成傳輸，確定要關閉 PairDrop？\",\n        \"offline\": \"你未連接到網絡\",\n        \"online\": \"已重新連線到網絡\",\n        \"connected\": \"已連線\",\n        \"online-requirement-pairing\": \"需要網絡連線嚟配對裝置\",\n        \"online-requirement-public-room\": \"需要網絡連線嚟建立公共房間\",\n        \"connecting\": \"連線中…\",\n        \"files-incorrect\": \"檔案不正確\",\n        \"file-transfer-completed\": \"檔案傳輸已完成\",\n        \"ios-memory-limit\": \"傳送至 iOS 嘅檔案單次上限為 200 MB\",\n        \"message-transfer-completed\": \"訊息傳輸已完成\",\n        \"rate-limit-join-key\": \"已達連線上限，請 10 秒後再試。\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"收到檔案\",\n        \"file-received-plural\": \"收到 {{count}} 個檔案\",\n        \"file-transfer-requested\": \"檔案傳輸請求\",\n        \"image-transfer-requested\": \"圖片傳輸請求\",\n        \"message-received\": \"收到訊息\",\n        \"message-received-plural\": \"收到 {{count}} 則訊息\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"點擊傳送 {{descriptor}}\",\n        \"click-to-send\": \"點擊傳送檔案 或 右鍵傳送訊息\",\n        \"connection-hash\": \"要驗證端到端加密安全性，請比較兩部裝置嘅安全編號\",\n        \"preparing\": \"準備中…\",\n        \"waiting\": \"請稍候…\",\n        \"processing\": \"處理中…\",\n        \"transferring\": \"傳輸中…\"\n    }\n}\n"
  },
  {
    "path": "public/lang/zh-TW.json",
    "content": "{\n    \"header\": {\n        \"language-selector_title\": \"設定語言\",\n        \"theme-auto_title\": \"自動使用系統主題\",\n        \"cancel-share-mode\": \"取消\",\n        \"edit-share-mode\": \"編輯\",\n        \"expand_title\": \"展開標題按鈕列\",\n        \"about_title\": \"關於 PairDrop\",\n        \"about_aria-label\": \"開啟關於 PairDrop\",\n        \"theme-dark_title\": \"總是使用深色主題\",\n        \"notification_title\": \"啟用通知\",\n        \"install_title\": \"安裝 PairDrop\",\n        \"pair-device_title\": \"永久配對你的裝置\",\n        \"edit-paired-devices_title\": \"編輯已配對的裝置\",\n        \"join-public-room_title\": \"暫時加入公共房間\",\n        \"theme-light_title\": \"總是使用淺色主題\"\n    },\n    \"instructions\": {\n        \"no-peers_data-drop-bg\": \"釋放以選擇接收者\",\n        \"no-peers-subtitle\": \"配對裝置或加入一個公共房間以便在其他網路上可見\",\n        \"x-instructions_mobile\": \"輕觸以發送檔案或輕觸並按住以發送一則訊息\",\n        \"x-instructions_data-drop-bg\": \"釋放以選擇接收者\",\n        \"x-instructions-share-mode_desktop\": \"點擊以發送 {{descriptor}}\",\n        \"activate-share-mode-and-other-file\": \"和另外 1 個檔案\",\n        \"activate-share-mode-and-other-files-plural\": \"和另外 {{count}} 個檔案\",\n        \"activate-share-mode-shared-text\": \"已分享的文字\",\n        \"activate-share-mode-shared-file\": \"已分享的檔案\",\n        \"webrtc-requirement\": \"要使用此 PairDrop 實例，必須啟用 WebRTC！\",\n        \"no-peers-title\": \"在其他裝置上開啟 PairDrop 以傳送檔案\",\n        \"x-instructions_desktop\": \"點擊以發送檔案或右鍵點擊以發送一則訊息\",\n        \"x-instructions_data-drop-peer\": \"釋放以發送到此裝置\",\n        \"x-instructions-share-mode_mobile\": \"輕觸以發送 {{descriptor}}\",\n        \"activate-share-mode-base\": \"在其他裝置上開啟 PairDrop 以傳送\",\n        \"activate-share-mode-shared-files-plural\": \"{{count}} 個已分享的檔案\"\n    },\n    \"footer\": {\n        \"display-name_data-placeholder\": \"正在載入中…\",\n        \"known-as\": \"此裝置名稱為：\",\n        \"display-name_title\": \"編輯此裝置名稱並儲存\",\n        \"discovery\": \"可見於：\",\n        \"on-this-network\": \"在這個網路上\",\n        \"paired-devices_title\": \"無論在任何網路，已配對的裝置都是隨時可見。\",\n        \"public-room-devices\": \"在房間 {{roomId}} 中\",\n        \"traffic\": \"流量將會\",\n        \"on-this-network_title\": \"在這個網路上的每個人都可以見到你。\",\n        \"paired-devices\": \"已配對的裝置\",\n        \"public-room-devices_title\": \"無論在任何網路，此公共房間中的裝置都是隨時可見。\",\n        \"routed\": \"途經伺服器\",\n        \"webrtc\": \"如果無法使用 WebRTC。\"\n    },\n    \"dialogs\": {\n        \"pair-devices-title\": \"永久配對裝置\",\n        \"scan-qr-code\": \"或掃描 QR 圖碼。\",\n        \"input-key-on-this-device\": \"在另一台裝置上輸入此密鑰\",\n        \"temporary-public-room-title\": \"臨時公共房間\",\n        \"input-room-id-on-another-device\": \"在另一台裝置上輸入此房間編號\",\n        \"enter-room-id-from-another-device\": \"在此輸入房間編號以加入房間。\",\n        \"hr-or\": \"或者\",\n        \"pair\": \"配對\",\n        \"cancel\": \"取消\",\n        \"paired-devices-wrapper_data-empty\": \"沒有已配對的裝置。\",\n        \"auto-accept-instructions-1\": \"啟用\",\n        \"auto-accept\": \"自動接收\",\n        \"auto-accept-instructions-2\": \"以自動接受從該裝置發送的所有檔案。\",\n        \"close\": \"關閉\",\n        \"join\": \"加入\",\n        \"send\": \"發送\",\n        \"receive-text-title\": \"收到訊息\",\n        \"base64-tap-to-paste\": \"輕觸此處以分享 {{type}}\",\n        \"base64-text\": \"文字\",\n        \"base64-files\": \"檔案\",\n        \"file-other-description-file\": \"和另外 1 個檔案\",\n        \"file-other-description-image-plural\": \"和另外 {{count}} 張圖片\",\n        \"file-other-description-file-plural\": \"和另外 {{count}} 個檔案\",\n        \"title-image\": \"圖片\",\n        \"title-file\": \"檔案\",\n        \"title-image-plural\": \"圖片\",\n        \"title-file-plural\": \"檔案\",\n        \"receive-title\": \"收到 {{descriptor}}\",\n        \"download-again\": \"再次下載\",\n        \"language-selector-title\": \"設定語言\",\n        \"system-language\": \"使用系統語言\",\n        \"public-room-qr-code_title\": \"點擊以複製連結到公共房間\",\n        \"pair-devices-qr-code_title\": \"點擊以複製連結以配對此裝置\",\n        \"approve\": \"批准\",\n        \"share-text-title\": \"分享文字訊息\",\n        \"share-text-subtitle\": \"發送前編輯訊息：\",\n        \"share-text-checkbox\": \"分享文字時總是顯示此對話框\",\n        \"enter-key-from-another-device\": \"在此輸入另一台裝置的密鑰。\",\n        \"edit-paired-devices-title\": \"編輯已配對的裝置\",\n        \"unpair\": \"取消配對\",\n        \"paired-device-removed\": \"已刪除已配對的裝置。\",\n        \"leave\": \"離開\",\n        \"accept\": \"接受\",\n        \"would-like-to-share\": \"想要分享\",\n        \"decline\": \"拒絕\",\n        \"has-sent\": \"已發送：\",\n        \"share\": \"分享\",\n        \"message_title\": \"輸入要傳送的訊息\",\n        \"download\": \"下載\",\n        \"send-message-to\": \"發送至：\",\n        \"message_placeholder\": \"文字\",\n        \"copy\": \"複製\",\n        \"send-message-title\": \"發送訊息\",\n        \"base64-title-files\": \"分享檔案\",\n        \"base64-title-text\": \"分享文字\",\n        \"base64-processing\": \"正在處理中…\",\n        \"base64-paste-to-send\": \"將剪貼簿貼到此處以分享 {{type}}\",\n        \"file-other-description-image\": \"和另外 1 張圖片\",\n        \"close-toast_title\": \"關閉通知\"\n    },\n    \"about\": {\n        \"claim\": \"跨裝置傳輸檔案的最簡單方法\",\n        \"github_title\": \"GitHub 上的 PairDrop\",\n        \"tweet_title\": \"發佈關於 PairDrop 的推文\",\n        \"mastodon_title\": \"在 Mastodon 上撰寫有關 PairDrop 的文章\",\n        \"close-about_aria-label\": \"關閉關於 PairDrop\",\n        \"buy-me-a-coffee_title\": \"給我買一杯咖啡吧！\",\n        \"bluesky_title\": \"在 Bluesky 上關注我們\",\n        \"custom_title\": \"關注我們\",\n        \"privacypolicy_title\": \"開啟我們的隱私權政策\",\n        \"faq_title\": \"常見問題\"\n    },\n    \"notifications\": {\n        \"display-name-changed-permanently\": \"裝置名稱已更改並儲存\",\n        \"display-name-changed-temporarily\": \"裝置名稱已更改並只限於此會話\",\n        \"display-name-random-again\": \"裝置名稱已再次隨機生成\",\n        \"download-successful\": \"{{descriptor}} 已下載\",\n        \"pairing-success\": \"裝置已配對\",\n        \"pairing-not-persistent\": \"已配對的裝置不是永久的\",\n        \"pairing-key-invalid\": \"無效的密鑰\",\n        \"pairing-key-invalidated\": \"密鑰 {{key}} 已失效\",\n        \"pairing-cleared\": \"所有裝置均已解除配對\",\n        \"public-room-id-invalid\": \"無效的房間編號\",\n        \"public-room-left\": \"已離開公共房間 {{publicRoomId}}\",\n        \"copied-to-clipboard\": \"已複製到剪貼簿\",\n        \"pair-url-copied-to-clipboard\": \"已將與此裝置配對的連結複製到剪貼簿\",\n        \"room-url-copied-to-clipboard\": \"已將公共房間的連結複製到剪貼簿\",\n        \"copied-to-clipboard-error\": \"無法複製。請手動複製。\",\n        \"text-content-incorrect\": \"文字內容不正確\",\n        \"file-content-incorrect\": \"檔案內容不正確\",\n        \"notifications-enabled\": \"已啟用通知\",\n        \"notifications-permissions-error\": \"由於使用者多次取消權限請求提示，通知權限已被封鎖。這可以在頁面資訊中重置，點擊網址欄旁邊的鎖定圖示以存取頁面資訊。\",\n        \"link-received\": \"收到 {{name}} 的連結—點擊以開啟\",\n        \"message-received\": \"收到 {{name}} 的訊息—點擊以複製\",\n        \"click-to-show\": \"點擊以顯示\",\n        \"copied-text\": \"已將文字複製到剪貼簿\",\n        \"copied-text-error\": \"無法複製到剪貼簿。請手動複製！\",\n        \"offline\": \"已離線\",\n        \"online\": \"已重新在線\",\n        \"connected\": \"已連接\",\n        \"online-requirement-pairing\": \"你需要在線才能配對裝置\",\n        \"online-requirement-public-room\": \"你需要在線才能創建公共房間\",\n        \"connecting\": \"正在連接中…\",\n        \"files-incorrect\": \"檔案不正確\",\n        \"file-transfer-completed\": \"檔案傳輸已完成\",\n        \"ios-memory-limit\": \"向 iOS 發送檔案每次只能最大 200 MB\",\n        \"message-transfer-completed\": \"訊息傳輸已完成\",\n        \"unfinished-transfers-warning\": \"還有未完成的傳輸。你確定要關閉 PairDrop 嗎？\",\n        \"rate-limit-join-key\": \"已達到速率限制。請等待 10 秒鐘，然後再試一次。\",\n        \"selected-peer-left\": \"選定的對象已離開\",\n        \"pairing-tabs-error\": \"將兩個網頁瀏覽器分頁進行配對是不可能的\",\n        \"clipboard-content-incorrect\": \"剪貼簿內容不正確\",\n        \"click-to-download\": \"點擊以下載\",\n        \"request-title\": \"{{name}} 想要發送 {{count}} 個 {{descriptor}}\"\n    },\n    \"document-titles\": {\n        \"file-received\": \"檔案已收到\",\n        \"file-received-plural\": \"已收到 {{count}} 個檔案\",\n        \"file-transfer-requested\": \"已請求檔案傳輸\",\n        \"image-transfer-requested\": \"已請求圖片傳輸\",\n        \"message-received\": \"已收到訊息\",\n        \"message-received-plural\": \"已收到 {{count}} 條訊息\"\n    },\n    \"peer-ui\": {\n        \"click-to-send-share-mode\": \"點擊以發送 {{descriptor}}\",\n        \"click-to-send\": \"點擊以發送檔案或右鍵點擊以發送一則訊息\",\n        \"preparing\": \"正在準備中…\",\n        \"waiting\": \"正在等待中…\",\n        \"processing\": \"正在處理中…\",\n        \"transferring\": \"正在傳輸中…\",\n        \"connection-hash\": \"若要驗證端對端加密的安全性，請在兩台裝置上比較此安全編號\"\n    }\n}\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n    \"name\": \"PairDrop\",\n    \"short_name\": \"PairDrop\",\n    \"icons\": [\n        {\n            \"src\": \"images/android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"images/android-chrome-512x512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"images/android-chrome-192x192-maskable.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        },\n        {\n            \"src\": \"images/android-chrome-512x512-maskable.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        }\n    ],\n    \"background_color\": \"#efefef\",\n    \"start_url\": \"./\",\n    \"display\": \"standalone\",\n    \"theme_color\": \"#3367d6\",\n    \"screenshots\" : [\n        {\n            \"src\": \"images/pairdrop_screenshot_mobile_1.png\",\n            \"sizes\": \"1170x2532\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"images/pairdrop_screenshot_mobile_2.png\",\n            \"sizes\": \"1170x2532\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"images/pairdrop_screenshot_mobile_3.png\",\n            \"sizes\": \"1170x2532\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"images/pairdrop_screenshot_mobile_4.png\",\n            \"sizes\": \"1170x2532\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"images/pairdrop_screenshot_mobile_5.png\",\n            \"sizes\": \"1170x2532\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"images/pairdrop_screenshot_mobile_6.png\",\n            \"sizes\": \"1170x2532\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"images/pairdrop_screenshot_mobile_7.png\",\n            \"sizes\": \"1170x2532\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"images/pairdrop_screenshot_mobile_8.png\",\n            \"sizes\": \"1170x2532\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"share_target\": {\n        \"action\": \"/\",\n        \"method\":\"POST\",\n        \"enctype\": \"multipart/form-data\",\n        \"params\": {\n            \"title\": \"title\",\n            \"text\": \"text\",\n            \"url\": \"url\",\n            \"files\": [{\n                \"name\": \"allfiles\",\n                \"accept\": [\"*/*\"]\n            }]\n        }\n    },\n    \"launch_handler\": {\n        \"client_mode\": \"focus-existing\"\n    }\n}\n"
  },
  {
    "path": "public/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "public/scripts/browser-tabs-connector.js",
    "content": "class BrowserTabsConnector {\n    constructor() {\n        if (!('BroadcastChannel' in window)) return;\n\n        this.bc = new BroadcastChannel('pairdrop');\n        this.bc.addEventListener('message', e => this._onMessage(e));\n        Events.on('broadcast-send', e => this._broadcastSend(e.detail));\n    }\n\n    _broadcastSend(message) {\n        this.bc.postMessage(message);\n    }\n\n    _onMessage(e) {\n        console.log('Broadcast:', e.data)\n        switch (e.data.type) {\n            case 'self-display-name-changed':\n                Events.fire('self-display-name-changed', e.data.detail);\n                break;\n        }\n    }\n\n    static peerIsSameBrowser(peerId) {\n        let peerIdsBrowser = JSON.parse(localStorage.getItem('peer_ids_browser'));\n        return peerIdsBrowser\n            ? peerIdsBrowser.indexOf(peerId) !== -1\n            : false;\n    }\n\n    static async addPeerIdToLocalStorage() {\n        const peerId = sessionStorage.getItem('peer_id');\n        if (!peerId) return false;\n\n        let peerIdsBrowser = [];\n        let peerIdsBrowserOld = JSON.parse(localStorage.getItem('peer_ids_browser'));\n\n        if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld);\n        peerIdsBrowser.push(peerId);\n        peerIdsBrowser = peerIdsBrowser.filter(onlyUnique);\n        localStorage.setItem('peer_ids_browser', JSON.stringify(peerIdsBrowser));\n\n        return peerIdsBrowser;\n    }\n\n    static async removePeerIdFromLocalStorage(peerId) {\n        let peerIdsBrowser = JSON.parse(localStorage.getItem('peer_ids_browser'));\n        const index = peerIdsBrowser.indexOf(peerId);\n        peerIdsBrowser.splice(index, 1);\n        localStorage.setItem('peer_ids_browser', JSON.stringify(peerIdsBrowser));\n        return peerId;\n    }\n\n\n    static async removeOtherPeerIdsFromLocalStorage() {\n        const peerId = sessionStorage.getItem('peer_id');\n        if (!peerId) return false;\n\n        let peerIdsBrowser = [peerId];\n        localStorage.setItem('peer_ids_browser', JSON.stringify(peerIdsBrowser));\n        return peerIdsBrowser;\n    }\n}"
  },
  {
    "path": "public/scripts/localization.js",
    "content": "class Localization {\n    constructor() {\n        Localization.$htmlRoot = document.querySelector('html');\n\n        Localization.defaultLocale = \"en\";\n        Localization.supportedLocales = [\n            \"ar\", \"be\", \"bg\", \"ca\", \"cs\", \"da\", \"de\", \"en\", \"es\", \"et\", \"eu\", \"fa\", \"fr\", \"he\", \"hu\", \"id\", \"it\", \"ja\",\n            \"kn\", \"ko\", \"nb\", \"nl\", \"nn\", \"pl\", \"pt-BR\", \"ro\", \"ru\", \"sk\", \"ta\", \"tr\", \"uk\", \"zh-CN\", \"zh-HK\", \"zh-TW\"\n        ];\n        Localization.supportedLocalesRtl = [\"ar\", \"he\"];\n\n        Localization.translations = {};\n        Localization.translationsDefaultLocale = {};\n\n        Localization.systemLocale = Localization.getSupportedOrDefaultLocales(navigator.languages);\n\n        let storedLanguageCode = localStorage.getItem('language_code');\n\n        Localization.initialLocale = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode)\n            ? storedLanguageCode\n            : Localization.systemLocale;\n    }\n\n    static localeIsSupported(locale) {\n        return Localization.supportedLocales.indexOf(locale) > -1;\n    }\n\n    static localeIsRtl(locale) {\n        return Localization.supportedLocalesRtl.indexOf(locale) > -1;\n    }\n\n    static currentLocaleIsRtl() {\n        return Localization.localeIsRtl(Localization.locale);\n    }\n\n    static currentLocaleIsDefault() {\n        return Localization.locale === Localization.defaultLocale\n    }\n\n    static getSupportedOrDefaultLocales(locales) {\n        // get generic locales not included in locales\n        // [\"en-us\", \"de-CH\", \"fr\"] --> [\"en\", \"de\"]\n        let localesGeneric = locales\n            .map(locale => locale.split(\"-\")[0])\n            .filter(locale => locales.indexOf(locale) === -1);\n\n        // If there is no perfect match for browser locales, try generic locales first before resorting to the default locale\n        return locales.find(Localization.localeIsSupported)\n            || localesGeneric.find(Localization.localeIsSupported)\n            || Localization.defaultLocale;\n    }\n\n    async setInitialTranslation() {\n        await Localization.fetchDefaultTranslations();\n        await Localization.setTranslation(Localization.initialLocale)\n    }\n\n    static async setTranslation(locale) {\n        if (!locale) locale = Localization.systemLocale;\n\n        await Localization.fetchTranslations(locale)\n        await Localization.translatePage();\n\n        if (Localization.localeIsRtl(locale)) {\n            Localization.$htmlRoot.setAttribute('dir', 'rtl');\n        }\n        else {\n            Localization.$htmlRoot.removeAttribute('dir');\n        }\n\n        Localization.$htmlRoot.setAttribute('lang', locale);\n\n\n        console.log(\"Page successfully translated\",\n            `System language: ${Localization.systemLocale}`,\n            `Selected language: ${locale}`\n        );\n\n        Events.fire(\"translation-loaded\");\n    }\n\n    static async fetchDefaultTranslations() {\n        Localization.translationsDefaultLocale = await Localization.fetchTranslationsFor(Localization.defaultLocale);\n    }\n\n    static async fetchTranslations(newLocale) {\n        if (newLocale === Localization.locale) return false;\n\n        const newTranslations = await Localization.fetchTranslationsFor(newLocale);\n\n        if(!newTranslations) return false;\n\n        Localization.locale = newLocale;\n        Localization.translations = newTranslations;\n    }\n\n    static getLocale() {\n        return Localization.locale;\n    }\n\n    static isSystemLocale() {\n        return !localStorage.getItem('language_code');\n    }\n\n    static async fetchTranslationsFor(newLocale) {\n        const response = await fetch(`lang/${newLocale}.json`, {\n            method: 'GET',\n            credentials: 'include',\n            mode: 'no-cors',\n        });\n\n        if (response.redirected === true || response.status !== 200) return false;\n\n        return await response.json();\n    }\n\n    static async translatePage() {\n        document\n            .querySelectorAll(\"[data-i18n-key]\")\n            .forEach(element => Localization.translateElement(element));\n    }\n\n    static async translateElement(element) {\n        const key = element.getAttribute(\"data-i18n-key\");\n        const attrs = element.getAttribute(\"data-i18n-attrs\").split(\" \");\n\n        attrs.forEach(attr => {\n            if (attr === \"text\") {\n                element.innerText = Localization.getTranslation(key);\n            }\n            else {\n                element.setAttribute(attr, Localization.getTranslation(key, attr));\n            }\n        })\n    }\n\n    static getTranslationFromTranslationsObj(translationObj, key, attr) {\n        let translation;\n        try {\n            const keys = key.split(\".\");\n\n            for (let i = 0; i < keys.length - 1; i++) {\n                // iterate into translation object until last layer\n                translationObj = translationObj[keys[i]]\n            }\n\n            let lastKey = keys[keys.length - 1];\n\n            if (attr) lastKey += \"_\" + attr;\n\n            translation = translationObj[lastKey];\n\n        } catch (e) {\n            console.error(e);\n        }\n\n        if (!translation) {\n            throw new Error(`Translation misses entry. Key: ${key} Attribute: ${attr}`);\n        }\n\n        return translation;\n    }\n\n    static addDataToTranslation(translation, data) {\n        for (let j in data) {\n            if (!translation.includes(`{{${j}}}`)) {\n                throw new Error(`Translation misses data placeholder: ${j}`);\n            }\n            // Add data to translation\n            translation = translation.replace(`{{${j}}}`, data[j]);\n        }\n        return translation;\n    }\n\n    static getTranslation(key, attr = null, data = {}, useDefault = false) {\n        let translationObj = useDefault\n            ? Localization.translationsDefaultLocale\n            : Localization.translations;\n\n        let translation;\n\n        try {\n            translation = Localization.getTranslationFromTranslationsObj(translationObj, key, attr);\n            translation = Localization.addDataToTranslation(translation, data);\n        }\n        catch (e) {\n            // Log warnings and help calls\n            console.warn(e);\n            Localization.logTranslationMissingOrBroken(key, attr, data, useDefault);\n            Localization.logHelpCallKey(key, attr);\n            Localization.logHelpCall();\n\n            if (useDefault || Localization.currentLocaleIsDefault()) {\n                // Is default locale already\n                // Use empty string as translation\n                translation = \"\"\n            }\n            else {\n                // Is not default locale yet\n                // Get translation for default language with same arguments\n                console.log(`Using default language ${Localization.defaultLocale.toUpperCase()} instead.`);\n                translation = this.getTranslation(key, attr, data, true);\n            }\n        }\n\n        return Localization.escapeHTML(translation);\n    }\n\n    static logTranslationMissingOrBroken(key, attr, data, useDefault) {\n        let usedLocale = useDefault\n            ? Localization.defaultLocale.toUpperCase()\n            : Localization.locale.toUpperCase();\n\n        console.warn(`Missing or broken translation for language ${usedLocale}.\\n`, 'key:', key, 'attr:', attr, 'data:', data);\n    }\n\n    static logHelpCall() {\n        console.log(\"Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/\");\n    }\n\n    static logHelpCallKey(key, attr) {\n        let locale = Localization.locale.toLowerCase();\n\n        let keyComplete = !attr || attr === \"text\"\n            ? key\n            : `${key}_${attr}`;\n\n        console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${locale}/?q=${keyComplete}`);\n    }\n\n    static escapeHTML(unsafeText) {\n        let div = document.createElement('div');\n        div.innerText = unsafeText;\n        return div.innerHTML;\n    }\n}\n"
  },
  {
    "path": "public/scripts/main.js",
    "content": "class PairDrop {\n\n    constructor() {\n        this.$headerNotificationBtn = $('notification');\n        this.$headerEditPairedDevicesBtn = $('edit-paired-devices');\n        this.$footerPairedDevicesBadge = $$('.discovery-wrapper .badge-room-secret');\n        this.$headerInstallBtn = $('install');\n\n        this.deferredStyles = [\n            \"styles/styles-deferred.css\"\n        ];\n        this.deferredScripts = [\n            \"scripts/browser-tabs-connector.js\",\n            \"scripts/util.js\",\n            \"scripts/network.js\",\n            \"scripts/ui.js\",\n            \"scripts/libs/heic2any.min.js\",\n            \"scripts/libs/no-sleep.min.js\",\n            \"scripts/libs/qr-code.min.js\",\n            \"scripts/libs/zip.min.js\"\n        ];\n\n        this.registerServiceWorker();\n\n        Events.on('beforeinstallprompt', e => this.onPwaInstallable(e));\n\n        this.persistentStorage = new PersistentStorage();\n        this.localization = new Localization();\n        this.themeUI = new ThemeUI();\n        this.backgroundCanvas = new BackgroundCanvas();\n        this.headerUI = new HeaderUI();\n        this.centerUI = new CenterUI();\n        this.footerUI = new FooterUI();\n\n        this.initialize()\n            .then(_ => {\n                console.log(\"Initialization completed.\");\n            });\n    }\n\n    async initialize() {\n        // Translate page before fading in\n        await this.localization.setInitialTranslation()\n        console.log(\"Initial translation successful.\");\n\n        // Show \"Loading...\" until connected to WsServer\n        await this.footerUI.showLoading();\n\n        // Evaluate css shifting UI elements and fade in UI elements\n        await this.evaluatePermissionsAndRoomSecrets();\n        await this.headerUI.evaluateOverflowing();\n        await this.headerUI.fadeIn();\n        await this.footerUI._evaluateFooterBadges();\n        await this.footerUI.fadeIn();\n        await this.centerUI.fadeIn();\n        await this.backgroundCanvas.fadeIn();\n\n        // Load deferred assets\n        console.log(\"Load deferred assets...\");\n        await this.loadDeferredAssets();\n        console.log(\"Loading of deferred assets completed.\");\n\n        console.log(\"Hydrate UI...\");\n        await this.hydrate();\n        console.log(\"UI hydrated.\");\n\n        // Evaluate url params as soon as ws is connected\n        console.log(\"Evaluate URL params as soon as websocket connection is established.\");\n        Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true});\n    }\n\n    registerServiceWorker() {\n        if ('serviceWorker' in navigator) {\n            navigator.serviceWorker\n                .register('service-worker.js')\n                .then(serviceWorker => {\n                    console.log('Service Worker registered');\n                    window.serviceWorker = serviceWorker\n                });\n        }\n    }\n\n    onPwaInstallable(e) {\n        if (!window.matchMedia('(display-mode: standalone)').matches) {\n            // only display install btn when not installed\n            this.$headerInstallBtn.removeAttribute('hidden');\n            this.$headerInstallBtn.addEventListener('click', () => {\n                this.$headerInstallBtn.setAttribute('hidden', true);\n                e.prompt();\n            });\n        }\n        return e.preventDefault();\n    }\n\n    async evaluatePermissionsAndRoomSecrets() {\n        // Check whether notification permissions have already been granted\n        if ('Notification' in window && Notification.permission !== 'granted') {\n            this.$headerNotificationBtn.removeAttribute('hidden');\n        }\n\n        let roomSecrets = await PersistentStorage.getAllRoomSecrets();\n        if (roomSecrets.length > 0) {\n            this.$headerEditPairedDevicesBtn.removeAttribute('hidden');\n            this.$footerPairedDevicesBadge.removeAttribute('hidden');\n        }\n    }\n\n    loadDeferredAssets() {\n        const stylePromises = this.deferredStyles.map(url => this.loadAndApplyStylesheet(url));\n        const scriptPromises = this.deferredScripts.map(url => this.loadAndApplyScript(url));\n\n        return Promise.all([...stylePromises, ...scriptPromises]);\n    }\n\n    loadStyleSheet(url) {\n        return new Promise((resolve, reject) => {\n            let stylesheet = document.createElement('link');\n            stylesheet.rel = 'preload';\n            stylesheet.as = 'style';\n            stylesheet.href = url;\n            stylesheet.onload = _ => {\n                stylesheet.onload = null;\n                stylesheet.rel = 'stylesheet';\n                resolve();\n            };\n            stylesheet.onerror = reject;\n\n            document.head.appendChild(stylesheet);\n        });\n    }\n\n    loadAndApplyStylesheet(url) {\n        return new Promise( async (resolve) => {\n            try {\n                await this.loadStyleSheet(url);\n                console.log(`Stylesheet loaded successfully: ${url}`);\n                resolve();\n            } catch (error) {\n                console.error('Error loading stylesheet:', error);\n            }\n        });\n    }\n\n    loadScript(url) {\n        return new Promise((resolve, reject) => {\n            let script = document.createElement(\"script\");\n            script.src = url;\n            script.onload = resolve;\n            script.onerror = reject;\n\n            document.body.appendChild(script);\n        });\n    }\n\n    loadAndApplyScript(url) {\n        return new Promise( async (resolve) => {\n            try {\n                await this.loadScript(url);\n                console.log(`Script loaded successfully: ${url}`);\n                resolve();\n            } catch (error) {\n                console.error('Error loading script:', error);\n            }\n        });\n    }\n\n    async hydrate() {\n        this.aboutUI = new AboutUI();\n        this.peersUI = new PeersUI();\n        this.languageSelectDialog = new LanguageSelectDialog();\n        this.receiveFileDialog = new ReceiveFileDialog();\n        this.receiveRequestDialog = new ReceiveRequestDialog();\n        this.sendTextDialog = new SendTextDialog();\n        this.receiveTextDialog = new ReceiveTextDialog();\n        this.pairDeviceDialog = new PairDeviceDialog();\n        this.clearDevicesDialog = new EditPairedDevicesDialog();\n        this.publicRoomDialog = new PublicRoomDialog();\n        this.base64Dialog = new Base64Dialog();\n        this.shareTextDialog = new ShareTextDialog();\n        this.toast = new Toast();\n        this.notifications = new Notifications();\n        this.networkStatusUI = new NetworkStatusUI();\n        this.webShareTargetUI = new WebShareTargetUI();\n        this.webFileHandlersUI = new WebFileHandlersUI();\n        this.noSleepUI = new NoSleepUI();\n        this.broadCast = new BrowserTabsConnector();\n        this.server = new ServerConnection();\n        this.peers = new PeersManager(this.server);\n    }\n\n    async evaluateUrlParams() {\n        // get url params\n        const urlParams = new URLSearchParams(window.location.search);\n        const hash = window.location.hash.substring(1);\n\n        // evaluate url params\n        if (urlParams.has('pair_key')) {\n            const pairKey = urlParams.get('pair_key');\n            this.pairDeviceDialog._pairDeviceJoin(pairKey);\n        }\n        else if (urlParams.has('room_id')) {\n            const roomId = urlParams.get('room_id');\n            this.publicRoomDialog._joinPublicRoom(roomId);\n        }\n        else if (urlParams.has('base64text')) {\n            const base64Text = urlParams.get('base64text');\n            await this.base64Dialog.evaluateBase64Text(base64Text, hash);\n        }\n        else if (urlParams.has('base64zip')) {\n            const base64Zip = urlParams.get('base64zip');\n            await this.base64Dialog.evaluateBase64Zip(base64Zip, hash);\n        }\n        else if (urlParams.has(\"share_target\")) {\n            const shareTargetType = urlParams.get(\"share_target\");\n            const title = urlParams.get('title') || '';\n            const text = urlParams.get('text') || '';\n            const url = urlParams.get('url') || '';\n            await this.webShareTargetUI.evaluateShareTarget(shareTargetType, title, text, url);\n        }\n        else if (urlParams.has(\"file_handler\")) {\n            await this.webFileHandlersUI.evaluateLaunchQueue();\n        }\n        else if (urlParams.has(\"init\")) {\n            const init = urlParams.get(\"init\");\n            if (init === \"pair\") {\n                this.pairDeviceDialog._pairDeviceInitiate();\n            }\n            else if (init === \"public_room\") {\n                this.publicRoomDialog._createPublicRoom();\n            }\n        }\n\n        // remove url params from url\n        const urlWithoutParams = getUrlWithoutArguments();\n        window.history.replaceState({}, \"Rewrite URL\", urlWithoutParams);\n\n        console.log(\"URL params evaluated.\");\n    }\n}\n\nconst pairDrop = new PairDrop();"
  },
  {
    "path": "public/scripts/network.js",
    "content": "class ServerConnection {\n\n    constructor() {\n        Events.on('pagehide', _ => this._disconnect());\n        Events.on(window.visibilityChangeEvent, _ => this._onVisibilityChange());\n\n        if (navigator.connection) {\n            navigator.connection.addEventListener('change', _ => this._reconnect());\n        }\n\n        Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));\n        Events.on('join-ip-room', _ => this.send({ type: 'join-ip-room'}));\n        Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));\n        Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));\n        Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());\n        Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));\n        Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));\n\n        Events.on('create-public-room', _ => this._onCreatePublicRoom());\n        Events.on('join-public-room', e => this._onJoinPublicRoom(e.detail.roomId, e.detail.createIfInvalid));\n        Events.on('leave-public-room', _ => this._onLeavePublicRoom());\n\n        Events.on('offline', _ => clearTimeout(this._reconnectTimer));\n        Events.on('online', _ => this._connect());\n\n        this._getConfig().then(() => this._connect());\n    }\n\n    _getConfig() {\n        console.log(\"Loading config...\")\n        return new Promise((resolve, reject) => {\n            let xhr = new XMLHttpRequest();\n            xhr.addEventListener(\"load\", () => {\n                if (xhr.status === 200) {\n                    // Config received\n                    let config = JSON.parse(xhr.responseText);\n                    console.log(\"Config loaded:\", config)\n                    this._config = config;\n                    Events.fire('config', config);\n                    resolve()\n                } else if (xhr.status < 200 || xhr.status >= 300) {\n                    retry(xhr);\n                }\n            })\n\n            xhr.addEventListener(\"error\", _ => {\n                retry(xhr);\n            });\n\n            function retry(request) {\n                setTimeout(function () {\n                    openAndSend(request)\n                }, 1000)\n            }\n\n            function openAndSend() {\n                xhr.open('GET', 'config');\n                xhr.send();\n            }\n\n            openAndSend(xhr);\n        })\n    }\n\n    _setWsConfig(wsConfig) {\n        this._wsConfig = wsConfig;\n        Events.fire('ws-config', wsConfig);\n    }\n\n    _connect() {\n        clearTimeout(this._reconnectTimer);\n        if (this._isConnected() || this._isConnecting() || this._isOffline()) return;\n        if (this._isReconnect) {\n            Events.fire('notify-user', {\n                message: Localization.getTranslation(\"notifications.connecting\"),\n                persistent: true\n            });\n        }\n        const ws = new WebSocket(this._endpoint());\n        ws.binaryType = 'arraybuffer';\n        ws.onopen = _ => this._onOpen();\n        ws.onmessage = e => this._onMessage(e.data);\n        ws.onclose = _ => this._onDisconnect();\n        ws.onerror = e => this._onError(e);\n        this._socket = ws;\n    }\n\n    _onOpen() {\n        console.log('WS: server connected');\n        Events.fire('ws-connected');\n        if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation(\"notifications.connected\"));\n    }\n\n    _onPairDeviceInitiate() {\n        if (!this._isConnected()) {\n            Events.fire('notify-user', Localization.getTranslation(\"notifications.online-requirement-pairing\"));\n            return;\n        }\n        this.send({ type: 'pair-device-initiate' });\n    }\n\n    _onPairDeviceJoin(pairKey) {\n        if (!this._isConnected()) {\n            setTimeout(() => this._onPairDeviceJoin(pairKey), 1000);\n            return;\n        }\n        this.send({ type: 'pair-device-join', pairKey: pairKey });\n    }\n\n    _onCreatePublicRoom() {\n        if (!this._isConnected()) {\n            Events.fire('notify-user', Localization.getTranslation(\"notifications.online-requirement-public-room\"));\n            return;\n        }\n        this.send({ type: 'create-public-room' });\n    }\n\n    _onJoinPublicRoom(roomId, createIfInvalid) {\n        if (!this._isConnected()) {\n            setTimeout(() => this._onJoinPublicRoom(roomId), 1000);\n            return;\n        }\n        this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid });\n    }\n\n    _onLeavePublicRoom() {\n        if (!this._isConnected()) {\n            setTimeout(() => this._onLeavePublicRoom(), 1000);\n            return;\n        }\n        this.send({ type: 'leave-public-room' });\n    }\n\n    _onMessage(msg) {\n        msg = JSON.parse(msg);\n        if (msg.type !== 'ping') console.log('WS receive:', msg);\n        switch (msg.type) {\n            case 'ws-config':\n                this._setWsConfig(msg.wsConfig);\n                break;\n            case 'peers':\n                this._onPeers(msg);\n                break;\n            case 'peer-joined':\n                Events.fire('peer-joined', msg);\n                break;\n            case 'peer-left':\n                Events.fire('peer-left', msg);\n                break;\n            case 'signal':\n                Events.fire('signal', msg);\n                break;\n            case 'ping':\n                this.send({ type: 'pong' });\n                break;\n            case 'display-name':\n                this._onDisplayName(msg);\n                break;\n            case 'pair-device-initiated':\n                Events.fire('pair-device-initiated', msg);\n                break;\n            case 'pair-device-joined':\n                Events.fire('pair-device-joined', msg);\n                break;\n            case 'pair-device-join-key-invalid':\n                Events.fire('pair-device-join-key-invalid');\n                break;\n            case 'pair-device-canceled':\n                Events.fire('pair-device-canceled', msg.pairKey);\n                break;\n            case 'join-key-rate-limit':\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.rate-limit-join-key\"));\n                break;\n            case 'secret-room-deleted':\n                Events.fire('secret-room-deleted', msg.roomSecret);\n                break;\n            case 'room-secret-regenerated':\n                Events.fire('room-secret-regenerated', msg);\n                break;\n            case 'public-room-id-invalid':\n                Events.fire('public-room-id-invalid', msg.publicRoomId);\n                break;\n            case 'public-room-created':\n                Events.fire('public-room-created', msg.roomId);\n                break;\n            case 'public-room-left':\n                Events.fire('public-room-left');\n                break;\n            case 'request':\n            case 'header':\n            case 'partition':\n            case 'partition-received':\n            case 'progress':\n            case 'files-transfer-response':\n            case 'file-transfer-complete':\n            case 'message-transfer-complete':\n            case 'text':\n            case 'display-name-changed':\n            case 'ws-chunk':\n                // ws-fallback\n                if (this._wsConfig.wsFallback) {\n                    Events.fire('ws-relay', JSON.stringify(msg));\n                }\n                else {\n                    console.log(\"WS receive: message type is for websocket fallback only but websocket fallback is not activated on this instance.\")\n                }\n                break;\n            default:\n                console.error('WS receive: unknown message type', msg);\n        }\n    }\n\n    send(msg) {\n        if (!this._isConnected()) return;\n        if (msg.type !== 'pong') console.log(\"WS send:\", msg)\n        this._socket.send(JSON.stringify(msg));\n    }\n\n    _onPeers(msg) {\n        Events.fire('peers', msg);\n    }\n\n    _onDisplayName(msg) {\n        // Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload\n        sessionStorage.setItem('peer_id', msg.peerId);\n        sessionStorage.setItem('peer_id_hash', msg.peerIdHash);\n\n        // Add peerId to localStorage to mark it for other PairDrop tabs on the same browser\n        BrowserTabsConnector\n            .addPeerIdToLocalStorage()\n            .then(peerId => {\n                if (!peerId) return;\n                console.log(\"successfully added peerId to localStorage\");\n\n                // Only now join rooms\n                Events.fire('join-ip-room');\n                PersistentStorage.getAllRoomSecrets()\n                    .then(roomSecrets => {\n                        Events.fire('room-secrets', roomSecrets);\n                    });\n            });\n\n        Events.fire('display-name', msg);\n    }\n\n    _endpoint() {\n        const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';\n        // Check whether the instance specifies another signaling server otherwise use the current instance for signaling\n        let wsServerDomain = this._config.signalingServer\n            ? this._config.signalingServer\n            : location.host + location.pathname;\n\n        let wsUrl = new URL(protocol + '://' + wsServerDomain + 'server');\n\n        wsUrl.searchParams.append('webrtc_supported', window.isRtcSupported ? 'true' : 'false');\n\n        const peerId = sessionStorage.getItem('peer_id');\n        const peerIdHash = sessionStorage.getItem('peer_id_hash');\n        if (peerId && peerIdHash) {\n            wsUrl.searchParams.append('peer_id', peerId);\n            wsUrl.searchParams.append('peer_id_hash', peerIdHash);\n        }\n\n        return wsUrl.toString();\n    }\n\n    _disconnect() {\n        this.send({ type: 'disconnect' });\n\n        const peerId = sessionStorage.getItem('peer_id');\n        BrowserTabsConnector\n            .removePeerIdFromLocalStorage(peerId)\n            .then(_ => {\n                console.log(\"successfully removed peerId from localStorage\");\n            });\n\n        if (!this._socket) return;\n\n        this._socket.onclose = null;\n        this._socket.close();\n        this._socket = null;\n        Events.fire('ws-disconnected');\n        this._isReconnect = true;\n    }\n\n    _onDisconnect() {\n        console.log('WS: server disconnected');\n        setTimeout(() => {\n            this._isReconnect = true;\n            Events.fire('ws-disconnected');\n            this._reconnectTimer = setTimeout(() => this._connect(), 1000);\n        }, 100); //delay for 100ms to prevent flickering on page reload\n    }\n\n    _onVisibilityChange() {\n        if (window.hiddenProperty) return;\n        this._connect();\n    }\n\n    _isConnected() {\n        return this._socket && this._socket.readyState === this._socket.OPEN;\n    }\n\n    _isConnecting() {\n        return this._socket && this._socket.readyState === this._socket.CONNECTING;\n    }\n\n    _isOffline() {\n        return !navigator.onLine;\n    }\n\n    _onError(e) {\n        console.error(e);\n    }\n\n    _reconnect() {\n        this._disconnect();\n        this._connect();\n    }\n}\n\nclass Peer {\n\n    constructor(serverConnection, isCaller, peerId, roomType, roomId) {\n        this._server = serverConnection;\n        this._isCaller = isCaller;\n        this._peerId = peerId;\n\n        this._roomIds = {};\n        this._updateRoomIds(roomType, roomId);\n\n        this._filesQueue = [];\n        this._busy = false;\n\n        // evaluate auto accept\n        this._evaluateAutoAccept();\n    }\n\n    sendJSON(message) {\n        this._send(JSON.stringify(message));\n    }\n\n    // Is overwritten in expanding classes\n    _send(message) {}\n\n    sendDisplayName(displayName) {\n        this.sendJSON({type: 'display-name-changed', displayName: displayName});\n    }\n\n    _isSameBrowser() {\n        return BrowserTabsConnector.peerIsSameBrowser(this._peerId);\n    }\n\n    _isPaired() {\n        return !!this._roomIds['secret'];\n    }\n\n    _getPairSecret() {\n        return this._roomIds['secret'];\n    }\n\n    _regenerationOfPairSecretNeeded() {\n        return this._getPairSecret() && this._getPairSecret().length !== 256\n    }\n\n    _getRoomTypes() {\n        return Object.keys(this._roomIds);\n    }\n\n    _updateRoomIds(roomType, roomId) {\n        const roomTypeIsSecret = roomType === \"secret\";\n        const roomIdIsNotPairSecret = this._getPairSecret() !== roomId;\n\n        // if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets\n        // -> do not delete duplicates and do not regenerate room secrets\n        if (!this._isSameBrowser()\n            && roomTypeIsSecret\n            && this._isPaired()\n            && roomIdIsNotPairSecret) {\n            // multiple roomSecrets with same peer -> delete old roomSecret\n            PersistentStorage\n                .deleteRoomSecret(this._getPairSecret())\n                .then(deletedRoomSecret => {\n                    if (deletedRoomSecret) console.log(\"Successfully deleted duplicate room secret with same peer: \", deletedRoomSecret);\n                });\n        }\n\n        this._roomIds[roomType] = roomId;\n\n        if (!this._isSameBrowser()\n            &&  roomTypeIsSecret\n            &&  this._isPaired()\n            &&  this._regenerationOfPairSecretNeeded()\n            &&  this._isCaller) {\n            // increase security by initiating the increase of the roomSecret length\n            // from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)\n            console.log('RoomSecret is regenerated to increase security')\n            Events.fire('regenerate-room-secret', this._getPairSecret());\n        }\n    }\n\n    _removeRoomType(roomType) {\n        delete this._roomIds[roomType];\n\n        Events.fire('room-type-removed', {\n            peerId: this._peerId,\n            roomType: roomType\n        });\n    }\n\n    _evaluateAutoAccept() {\n        if (!this._isPaired()) {\n            this._setAutoAccept(false);\n            return;\n        }\n\n        PersistentStorage\n            .getRoomSecretEntry(this._getPairSecret())\n            .then(roomSecretEntry => {\n                const autoAccept = roomSecretEntry\n                    ? roomSecretEntry.entry.auto_accept\n                    : false;\n                this._setAutoAccept(autoAccept);\n            })\n            .catch(_ => {\n                this._setAutoAccept(false);\n            });\n    }\n\n    _setAutoAccept(autoAccept) {\n        this._autoAccept = !this._isSameBrowser()\n            ? autoAccept\n            : false;\n    }\n\n    async requestFileTransfer(files) {\n        let header = [];\n        let totalSize = 0;\n        let imagesOnly = true\n        for (let i=0; i<files.length; i++) {\n            Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})\n            header.push({\n                name: files[i].name,\n                mime: files[i].type,\n                size: files[i].size\n            });\n            totalSize += files[i].size;\n            if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;\n        }\n\n        Events.fire('set-progress', {peerId: this._peerId, progress: 0.8, status: 'prepare'})\n\n        let dataUrl = '';\n        if (files[0].type.split('/')[0] === 'image') {\n            try {\n                dataUrl = await getThumbnailAsDataUrl(files[0], 400, null, 0.9);\n            } catch (e) {\n                console.error(e);\n            }\n        }\n\n        Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'prepare'})\n\n        this._filesRequested = files;\n\n        this.sendJSON({type: 'request',\n            header: header,\n            totalSize: totalSize,\n            imagesOnly: imagesOnly,\n            thumbnailDataUrl: dataUrl\n        });\n        Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'})\n    }\n\n    async sendFiles() {\n        for (let i=0; i<this._filesRequested.length; i++) {\n            this._filesQueue.push(this._filesRequested[i]);\n        }\n        this._filesRequested = null\n        if (this._busy) return;\n        this._dequeueFile();\n    }\n\n    _dequeueFile() {\n        this._busy = true;\n        const file = this._filesQueue.shift();\n        this._sendFile(file);\n    }\n\n    async _sendFile(file) {\n        this.sendJSON({\n            type: 'header',\n            size: file.size,\n            name: file.name,\n            mime: file.type\n        });\n        this._chunker = new FileChunker(file,\n            chunk => this._send(chunk),\n            offset => this._onPartitionEnd(offset));\n        this._chunker.nextPartition();\n    }\n\n    _onPartitionEnd(offset) {\n        this.sendJSON({ type: 'partition', offset: offset });\n    }\n\n    _onReceivedPartitionEnd(offset) {\n        this.sendJSON({ type: 'partition-received', offset: offset });\n    }\n\n    _sendNextPartition() {\n        if (!this._chunker || this._chunker.isFileEnd()) return;\n        this._chunker.nextPartition();\n    }\n\n    _sendProgress(progress) {\n        this.sendJSON({ type: 'progress', progress: progress });\n    }\n\n    _onMessage(message) {\n        if (typeof message !== 'string') {\n            this._onChunkReceived(message);\n            return;\n        }\n        const messageJSON = JSON.parse(message);\n        switch (messageJSON.type) {\n            case 'request':\n                this._onFilesTransferRequest(messageJSON);\n                break;\n            case 'header':\n                this._onFileHeader(messageJSON);\n                break;\n            case 'partition':\n                this._onReceivedPartitionEnd(messageJSON);\n                break;\n            case 'partition-received':\n                this._sendNextPartition();\n                break;\n            case 'progress':\n                this._onDownloadProgress(messageJSON.progress);\n                break;\n            case 'files-transfer-response':\n                this._onFileTransferRequestResponded(messageJSON);\n                break;\n            case 'file-transfer-complete':\n                this._onFileTransferCompleted();\n                break;\n            case 'message-transfer-complete':\n                this._onMessageTransferCompleted();\n                break;\n            case 'text':\n                this._onTextReceived(messageJSON);\n                break;\n            case 'display-name-changed':\n                this._onDisplayNameChanged(messageJSON);\n                break;\n        }\n    }\n\n    _onFilesTransferRequest(request) {\n        if (this._requestPending) {\n            // Only accept one request at a time per peer\n            this.sendJSON({type: 'files-transfer-response', accepted: false});\n            return;\n        }\n        if (window.iOS && request.totalSize >= 200*1024*1024) {\n            // iOS Safari can only put 400MB at once to memory.\n            // Request to send them in chunks of 200MB instead:\n            this.sendJSON({type: 'files-transfer-response', accepted: false, reason: 'ios-memory-limit'});\n            return;\n        }\n\n        this._requestPending = request;\n\n        if (this._autoAccept) {\n            // auto accept if set via Edit Paired Devices Dialog\n            this._respondToFileTransferRequest(true);\n            return;\n        }\n\n        // default behavior: show user transfer request\n        Events.fire('files-transfer-request', {\n            request: request,\n            peerId: this._peerId\n        });\n    }\n\n    _respondToFileTransferRequest(accepted) {\n        this.sendJSON({type: 'files-transfer-response', accepted: accepted});\n        if (accepted) {\n            this._requestAccepted = this._requestPending;\n            this._totalBytesReceived = 0;\n            this._busy = true;\n            this._filesReceived = [];\n        }\n        this._requestPending = null;\n    }\n\n    _onFileHeader(header) {\n        if (this._requestAccepted && this._requestAccepted.header.length) {\n            this._lastProgress = 0;\n            this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime},\n                this._requestAccepted.totalSize,\n                this._totalBytesReceived,\n                fileBlob => this._onFileReceived(fileBlob)\n            );\n        }\n    }\n\n    _abortTransfer() {\n        Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});\n        Events.fire('notify-user', Localization.getTranslation(\"notifications.files-incorrect\"));\n        this._filesReceived = [];\n        this._requestAccepted = null;\n        this._digester = null;\n        throw new Error(\"Received files differ from requested files. Abort!\");\n    }\n\n    _onChunkReceived(chunk) {\n        if(!this._digester || !(chunk.byteLength || chunk.size)) return;\n\n        this._digester.unchunk(chunk);\n        const progress = this._digester.progress;\n\n        if (progress > 1) {\n            this._abortTransfer();\n        }\n\n        this._onDownloadProgress(progress);\n\n        // occasionally notify sender about our progress\n        if (progress - this._lastProgress < 0.005 && progress !== 1) return;\n        this._lastProgress = progress;\n        this._sendProgress(progress);\n    }\n\n    _onDownloadProgress(progress) {\n        Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'});\n    }\n\n    async _onFileReceived(fileBlob) {\n        const acceptedHeader = this._requestAccepted.header.shift();\n        this._totalBytesReceived += fileBlob.size;\n\n        this.sendJSON({type: 'file-transfer-complete'});\n\n        const sameSize = fileBlob.size === acceptedHeader.size;\n        const sameName = fileBlob.name === acceptedHeader.name\n        if (!sameSize || !sameName) {\n            this._abortTransfer();\n        }\n\n        // include for compatibility with 'Snapdrop & PairDrop for Android' app\n        Events.fire('file-received', fileBlob);\n\n        this._filesReceived.push(fileBlob);\n        if (!this._requestAccepted.header.length) {\n            this._busy = false;\n            Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});\n            Events.fire('files-received', {peerId: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});\n            this._filesReceived = [];\n            this._requestAccepted = null;\n        }\n    }\n\n    _onFileTransferCompleted() {\n        this._chunker = null;\n        if (!this._filesQueue.length) {\n            this._busy = false;\n            Events.fire('notify-user', Localization.getTranslation(\"notifications.file-transfer-completed\"));\n            Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app\n        }\n        else {\n            this._dequeueFile();\n        }\n    }\n\n    _onFileTransferRequestResponded(message) {\n        if (!message.accepted) {\n            Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});\n            this._filesRequested = null;\n            if (message.reason === 'ios-memory-limit') {\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.ios-memory-limit\"));\n            }\n            return;\n        }\n        Events.fire('file-transfer-accepted');\n        Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'transfer'});\n        this.sendFiles();\n    }\n\n    _onMessageTransferCompleted() {\n        Events.fire('notify-user', Localization.getTranslation(\"notifications.message-transfer-completed\"));\n    }\n\n    sendText(text) {\n        const unescaped = btoa(unescape(encodeURIComponent(text)));\n        this.sendJSON({ type: 'text', text: unescaped });\n    }\n\n    _onTextReceived(message) {\n        if (!message.text) return;\n        const escaped = decodeURIComponent(escape(atob(message.text)));\n        Events.fire('text-received', { text: escaped, peerId: this._peerId });\n        this.sendJSON({ type: 'message-transfer-complete' });\n    }\n\n    _onDisplayNameChanged(message) {\n        const displayNameHasChanged = this._displayName !== message.displayName\n\n        if (message.displayName && displayNameHasChanged) {\n            this._displayName = message.displayName;\n        }\n\n        Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});\n\n        if (!displayNameHasChanged) return;\n        Events.fire('notify-peer-display-name-changed', this._peerId);\n    }\n}\n\nclass RTCPeer extends Peer {\n\n    constructor(serverConnection, isCaller, peerId, roomType, roomId, rtcConfig) {\n        super(serverConnection, isCaller, peerId, roomType, roomId);\n\n        this.rtcSupported = true;\n        this.rtcConfig = rtcConfig\n\n        if (!this._isCaller) return; // we will listen for a caller\n        this._connect();\n    }\n\n    _connect() {\n        if (!this._conn || this._conn.signalingState === \"closed\") this._openConnection();\n\n        if (this._isCaller) {\n            this._openChannel();\n        }\n        else {\n            this._conn.ondatachannel = e => this._onChannelOpened(e);\n        }\n    }\n\n    _openConnection() {\n        this._conn = new RTCPeerConnection(this.rtcConfig);\n        this._conn.onicecandidate = e => this._onIceCandidate(e);\n        this._conn.onicecandidateerror = e => this._onError(e);\n        this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();\n        this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);\n    }\n\n    _openChannel() {\n        if (!this._conn) return;\n\n        const channel = this._conn.createDataChannel('data-channel', {\n            ordered: true,\n            reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable\n        });\n        channel.onopen = e => this._onChannelOpened(e);\n        channel.onerror = e => this._onError(e);\n\n        this._conn\n            .createOffer()\n            .then(d => this._onDescription(d))\n            .catch(e => this._onError(e));\n    }\n\n    _onDescription(description) {\n        // description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');\n        this._conn\n            .setLocalDescription(description)\n            .then(_ => this._sendSignal({ sdp: description }))\n            .catch(e => this._onError(e));\n    }\n\n    _onIceCandidate(event) {\n        if (!event.candidate) return;\n        this._sendSignal({ ice: event.candidate });\n    }\n\n    onServerMessage(message) {\n        if (!this._conn) this._connect();\n\n        if (message.sdp) {\n            this._conn\n                .setRemoteDescription(message.sdp)\n                .then(_ => {\n                    if (message.sdp.type === 'offer') {\n                        return this._conn\n                            .createAnswer()\n                            .then(d => this._onDescription(d));\n                    }\n                })\n                .catch(e => this._onError(e));\n        }\n        else if (message.ice) {\n            this._conn\n                .addIceCandidate(new RTCIceCandidate(message.ice))\n                .catch(e => this._onError(e));\n        }\n    }\n\n    _onChannelOpened(event) {\n        console.log('RTC: channel opened with', this._peerId);\n        const channel = event.channel || event.target;\n        channel.binaryType = 'arraybuffer';\n        channel.onmessage = e => this._onMessage(e.data);\n        channel.onclose = _ => this._onChannelClosed();\n        this._channel = channel;\n        Events.on('beforeunload', e => this._onBeforeUnload(e));\n        Events.on('pagehide', _ => this._onPageHide());\n        Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});\n    }\n\n    _onMessage(message) {\n        if (typeof message === 'string') {\n            console.log('RTC:', JSON.parse(message));\n        }\n        super._onMessage(message);\n    }\n\n    getConnectionHash() {\n        const localDescriptionLines = this._conn.localDescription.sdp.split(\"\\r\\n\");\n        const remoteDescriptionLines = this._conn.remoteDescription.sdp.split(\"\\r\\n\");\n        let localConnectionFingerprint, remoteConnectionFingerprint;\n        for (let i=0; i<localDescriptionLines.length; i++) {\n            if (localDescriptionLines[i].startsWith(\"a=fingerprint:\")) {\n                localConnectionFingerprint = localDescriptionLines[i].substring(14);\n                break;\n            }\n        }\n        for (let i=0; i<remoteDescriptionLines.length; i++) {\n            if (remoteDescriptionLines[i].startsWith(\"a=fingerprint:\")) {\n                remoteConnectionFingerprint = remoteDescriptionLines[i].substring(14);\n                break;\n            }\n        }\n        const combinedFingerprints = this._isCaller\n            ? localConnectionFingerprint + remoteConnectionFingerprint\n            : remoteConnectionFingerprint + localConnectionFingerprint;\n        let hash = cyrb53(combinedFingerprints).toString();\n        while (hash.length < 16) {\n            hash = \"0\" + hash;\n        }\n        return hash;\n    }\n\n    _onBeforeUnload(e) {\n        if (this._busy) {\n            e.preventDefault();\n            return Localization.getTranslation(\"notifications.unfinished-transfers-warning\");\n        }\n    }\n\n    _onPageHide() {\n        this._disconnect();\n    }\n\n    _disconnect() {\n        if (this._conn && this._channel) {\n            this._channel.onclose = null;\n            this._channel.close();\n        }\n        Events.fire('peer-disconnected', this._peerId);\n    }\n\n    _onChannelClosed() {\n        console.log('RTC: channel closed', this._peerId);\n        Events.fire('peer-disconnected', this._peerId);\n        if (!this._isCaller) return;\n        this._connect(); // reopen the channel\n    }\n\n    _onConnectionStateChange() {\n        console.log('RTC: state changed:', this._conn.connectionState);\n        switch (this._conn.connectionState) {\n            case 'disconnected':\n                Events.fire('peer-disconnected', this._peerId);\n                this._onError('rtc connection disconnected');\n                break;\n            case 'failed':\n                Events.fire('peer-disconnected', this._peerId);\n                this._onError('rtc connection failed');\n                break;\n        }\n    }\n\n    _onIceConnectionStateChange() {\n        switch (this._conn.iceConnectionState) {\n            case 'failed':\n                this._onError('ICE Gathering failed');\n                break;\n            default:\n                console.log('ICE Gathering', this._conn.iceConnectionState);\n        }\n    }\n\n    _onError(error) {\n        console.error(error);\n    }\n\n    _send(message) {\n        if (!this._channel) this.refresh();\n        this._channel.send(message);\n    }\n\n    _sendSignal(signal) {\n        signal.type = 'signal';\n        signal.to = this._peerId;\n        signal.roomType = this._getRoomTypes()[0];\n        signal.roomId = this._roomIds[this._getRoomTypes()[0]];\n        this._server.send(signal);\n    }\n\n    refresh() {\n        // check if channel is open. otherwise create one\n        if (this._isConnected() || this._isConnecting()) return;\n\n        // only reconnect if peer is caller\n        if (!this._isCaller) return;\n\n        this._connect();\n    }\n\n    _isConnected() {\n        return this._channel && this._channel.readyState === 'open';\n    }\n\n    _isConnecting() {\n        return this._channel && this._channel.readyState === 'connecting';\n    }\n\n    sendDisplayName(displayName) {\n        if (!this._isConnected()) return;\n        super.sendDisplayName(displayName);\n    }\n}\n\nclass WSPeer extends Peer {\n\n    constructor(serverConnection, isCaller, peerId, roomType, roomId) {\n        super(serverConnection, isCaller, peerId, roomType, roomId);\n\n        this.rtcSupported = false;\n\n        if (!this._isCaller) return; // we will listen for a caller\n        this._sendSignal();\n    }\n\n    _send(chunk) {\n        this.sendJSON({\n            type: 'ws-chunk',\n            chunk: arrayBufferToBase64(chunk)\n        });\n    }\n\n    sendJSON(message) {\n        message.to = this._peerId;\n        message.roomType = this._getRoomTypes()[0];\n        message.roomId = this._roomIds[this._getRoomTypes()[0]];\n        this._server.send(message);\n    }\n\n    _sendSignal(connected = false) {\n        this.sendJSON({type: 'signal', connected: connected});\n    }\n\n    onServerMessage(message) {\n        this._peerId = message.sender.id;\n        Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})\n        if (message.connected) return;\n        this._sendSignal(true);\n    }\n\n    getConnectionHash() {\n        // Todo: implement SubtleCrypto asymmetric encryption and create connectionHash from public keys\n        return \"\";\n    }\n}\n\nclass PeersManager {\n\n    constructor(serverConnection) {\n        this.peers = {};\n        this._server = serverConnection;\n        Events.on('signal', e => this._onMessage(e.detail));\n        Events.on('peers', e => this._onPeers(e.detail));\n        Events.on('files-selected', e => this._onFilesSelected(e.detail));\n        Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))\n        Events.on('send-text', e => this._onSendText(e.detail));\n        Events.on('peer-left', e => this._onPeerLeft(e.detail));\n        Events.on('peer-joined', e => this._onPeerJoined(e.detail));\n        Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));\n        Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));\n\n        // this device closes connection\n        Events.on('room-secrets-deleted', e => this._onRoomSecretsDeleted(e.detail));\n        Events.on('leave-public-room', e => this._onLeavePublicRoom(e.detail));\n\n        // peer closes connection\n        Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));\n\n        Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));\n        Events.on('display-name', e => this._onDisplayName(e.detail.displayName));\n        Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));\n        Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));\n        Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));\n        Events.on('ws-disconnected', _ => this._onWsDisconnected());\n        Events.on('ws-relay', e => this._onWsRelay(e.detail));\n        Events.on('ws-config', e => this._onWsConfig(e.detail));\n    }\n\n    _onWsConfig(wsConfig) {\n        this._wsConfig = wsConfig;\n    }\n\n    _onMessage(message) {\n        const peerId = message.sender.id;\n        this.peers[peerId].onServerMessage(message);\n    }\n\n    _refreshPeer(peerId, roomType, roomId) {\n        if (!this._peerExists(peerId)) return false;\n\n        const peer = this.peers[peerId];\n        const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;\n        const roomIdsDiffer = peer._roomIds[roomType] !== roomId;\n\n        // if roomType or roomId for roomType differs peer is already connected\n        // -> only update roomSecret and reevaluate auto accept\n        if (roomTypesDiffer || roomIdsDiffer) {\n            peer._updateRoomIds(roomType, roomId);\n            peer._evaluateAutoAccept();\n\n            return true;\n        }\n\n        peer.refresh();\n\n        return true;\n    }\n\n    _createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) {\n        if (this._peerExists(peerId)) {\n            this._refreshPeer(peerId, roomType, roomId);\n            return;\n        }\n\n        if (window.isRtcSupported && rtcSupported) {\n            this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId, this._wsConfig.rtcConfig);\n        }\n        else if (this._wsConfig.wsFallback) {\n            this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId);\n        }\n        else {\n            console.warn(\"Websocket fallback is not activated on this instance.\\n\" +\n                \"Activate WebRTC in this browser or ask the admin of this instance to activate the websocket fallback.\")\n        }\n    }\n\n    _onPeerJoined(message) {\n        this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId, message.peer.rtcSupported);\n    }\n\n    _onPeers(message) {\n        message.peers.forEach(peer => {\n            this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId, peer.rtcSupported);\n        })\n    }\n\n    _onWsRelay(message) {\n        if (!this._wsConfig.wsFallback) return;\n\n        const messageJSON = JSON.parse(message);\n        if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);\n        this.peers[messageJSON.sender.id]._onMessage(message);\n    }\n\n    _onRespondToFileTransferRequest(detail) {\n        this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);\n    }\n\n    async _onFilesSelected(message) {\n        let files = mime.addMissingMimeTypesToFiles([...message.files]);\n        await this.peers[message.to].requestFileTransfer(files);\n    }\n\n    _onSendText(message) {\n        this.peers[message.to].sendText(message.text);\n    }\n\n    _onPeerLeft(message) {\n        if (this._peerExists(message.peerId) && this._webRtcSupported(message.peerId)) {\n            console.log('WSPeer left:', message.peerId);\n        }\n        if (message.disconnect === true) {\n            // if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately\n            this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);\n\n            // If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:\n            // Tidy up peerIds in localStorage\n            if (Object.keys(this.peers).length === 0) {\n                BrowserTabsConnector\n                    .removeOtherPeerIdsFromLocalStorage()\n                    .then(peerIds => {\n                        if (!peerIds) return;\n                        console.log(\"successfully removed other peerIds from localStorage\");\n                    });\n            }\n        }\n    }\n\n    _onPeerConnected(peerId) {\n        this._notifyPeerDisplayNameChanged(peerId);\n    }\n\n    _peerExists(peerId) {\n        return !!this.peers[peerId];\n    }\n\n    _webRtcSupported(peerId) {\n        return this.peers[peerId].rtcSupported\n    }\n\n    _onWsDisconnected() {\n        if (!this._wsConfig || !this._wsConfig.wsFallback) return;\n\n        for (const peerId in this.peers) {\n            if (!this._webRtcSupported(peerId)) {\n                Events.fire('peer-disconnected', peerId);\n            }\n        }\n    }\n\n    _onPeerDisconnected(peerId) {\n        const peer = this.peers[peerId];\n        delete this.peers[peerId];\n        if (!peer || !peer._conn) return;\n        if (peer._channel) peer._channel.onclose = null;\n        peer._conn.close();\n        peer._busy = false;\n        peer._roomIds = {};\n    }\n\n    _onRoomSecretsDeleted(roomSecrets) {\n        for (let i=0; i<roomSecrets.length; i++) {\n            this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecrets[i]);\n        }\n    }\n\n    _onLeavePublicRoom(publicRoomId) {\n        this._disconnectOrRemoveRoomTypeByRoomId('public-id', publicRoomId);\n    }\n\n    _onSecretRoomDeleted(roomSecret) {\n        this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecret);\n    }\n\n    _disconnectOrRemoveRoomTypeByRoomId(roomType, roomId) {\n        const peerIds = this._getPeerIdsFromRoomId(roomId);\n\n        if (!peerIds.length) return;\n\n        for (let i=0; i<peerIds.length; i++) {\n            this._disconnectOrRemoveRoomTypeByPeerId(peerIds[i], roomType);\n        }\n    }\n\n    _disconnectOrRemoveRoomTypeByPeerId(peerId, roomType) {\n        const peer = this.peers[peerId];\n\n        if (!peer) return;\n\n        if (peer._getRoomTypes().length > 1) {\n            peer._removeRoomType(roomType);\n        }\n        else {\n            Events.fire('peer-disconnected', peerId);\n        }\n    }\n\n    _onRoomSecretRegenerated(message) {\n        PersistentStorage\n            .updateRoomSecret(message.oldRoomSecret, message.newRoomSecret)\n            .then(_ => {\n                console.log(\"successfully regenerated room secret\");\n                Events.fire(\"room-secrets\", [message.newRoomSecret]);\n            })\n    }\n\n    _notifyPeersDisplayNameChanged(newDisplayName) {\n        this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;\n        for (const peerId in this.peers) {\n            this._notifyPeerDisplayNameChanged(peerId);\n        }\n    }\n\n    _notifyPeerDisplayNameChanged(peerId) {\n        const peer = this.peers[peerId];\n        if (!peer) return;\n        this.peers[peerId].sendDisplayName(this._displayName);\n    }\n\n    _onDisplayName(displayName) {\n        this._originalDisplayName = displayName;\n        // if the displayName has not been changed (yet) set the displayName to the original displayName\n        if (!this._displayName) this._displayName = displayName;\n    }\n\n    _onAutoAcceptUpdated(roomSecret, autoAccept) {\n        const peerId = this._getPeerIdsFromRoomId(roomSecret)[0];\n\n        if (!peerId) return;\n\n        this.peers[peerId]._setAutoAccept(autoAccept);\n    }\n\n    _getPeerIdsFromRoomId(roomId) {\n        if (!roomId) return [];\n\n        let peerIds = []\n        for (const peerId in this.peers) {\n            const peer = this.peers[peerId];\n\n            // peer must have same roomId.\n            if (Object.values(peer._roomIds).includes(roomId)) {\n                peerIds.push(peer._peerId);\n            }\n        }\n        return peerIds;\n    }\n}\n\nclass FileChunker {\n\n    constructor(file, onChunk, onPartitionEnd) {\n        this._chunkSize = 64000; // 64 KB\n        this._maxPartitionSize = 1e6; // 1 MB\n        this._offset = 0;\n        this._partitionSize = 0;\n        this._file = file;\n        this._onChunk = onChunk;\n        this._onPartitionEnd = onPartitionEnd;\n        this._reader = new FileReader();\n        this._reader.addEventListener('load', e => this._onChunkRead(e.target.result));\n    }\n\n    nextPartition() {\n        this._partitionSize = 0;\n        this._readChunk();\n    }\n\n    _readChunk() {\n        const chunk = this._file.slice(this._offset, this._offset + this._chunkSize);\n        this._reader.readAsArrayBuffer(chunk);\n    }\n\n    _onChunkRead(chunk) {\n        this._offset += chunk.byteLength;\n        this._partitionSize += chunk.byteLength;\n        this._onChunk(chunk);\n        if (this.isFileEnd()) return;\n        if (this._isPartitionEnd()) {\n            this._onPartitionEnd(this._offset);\n            return;\n        }\n        this._readChunk();\n    }\n\n    repeatPartition() {\n        this._offset -= this._partitionSize;\n        this.nextPartition();\n    }\n\n    _isPartitionEnd() {\n        return this._partitionSize >= this._maxPartitionSize;\n    }\n\n    isFileEnd() {\n        return this._offset >= this._file.size;\n    }\n}\n\nclass FileDigester {\n\n    constructor(meta, totalSize, totalBytesReceived, callback) {\n        this._buffer = [];\n        this._bytesReceived = 0;\n        this._size = meta.size;\n        this._name = meta.name;\n        this._mime = meta.mime;\n        this._totalSize = totalSize;\n        this._totalBytesReceived = totalBytesReceived;\n        this._callback = callback;\n    }\n\n    unchunk(chunk) {\n        this._buffer.push(chunk);\n        this._bytesReceived += chunk.byteLength || chunk.size;\n        this.progress = (this._totalBytesReceived + this._bytesReceived) / this._totalSize;\n        if (isNaN(this.progress)) this.progress = 1\n\n        if (this._bytesReceived < this._size) return;\n        // we are done\n        const blob = new Blob(this._buffer)\n        this._buffer = null;\n        this._callback(new File([blob], this._name, {\n            type: this._mime || \"application/octet-stream\",\n            lastModified: new Date().getTime()\n        }));\n    }\n}\n"
  },
  {
    "path": "public/scripts/persistent-storage.js",
    "content": "class PersistentStorage {\n    constructor() {\n        if (!('indexedDB' in window)) {\n            PersistentStorage.logBrowserNotCapable();\n            return;\n        }\n        const DBOpenRequest = window.indexedDB.open('pairdrop_store', 5);\n        DBOpenRequest.onerror = e => {\n            PersistentStorage.logBrowserNotCapable();\n            console.log('Error initializing database: ');\n            console.log(e)\n        };\n        DBOpenRequest.onsuccess = _ => {\n            console.log('Database initialised.');\n        };\n        DBOpenRequest.onupgradeneeded = async e => {\n            const db = e.target.result;\n            const txn = e.target.transaction;\n\n            db.onerror = e => console.log('Error loading database: ' + e);\n\n            console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);\n\n            if (e.oldVersion === 0) {\n                // initiate v1\n                db.createObjectStore('keyval');\n                let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true});\n                roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true });\n            }\n            if (e.oldVersion <= 1) {\n                // migrate to v2\n                db.createObjectStore('share_target_files');\n            }\n            if (e.oldVersion <= 2) {\n                // migrate to v3\n                db.deleteObjectStore('share_target_files');\n                db.createObjectStore('share_target_files', {autoIncrement: true});\n            }\n            if (e.oldVersion <= 3) {\n                // migrate to v4\n                let roomSecretsObjectStore4 = txn.objectStore('room_secrets');\n                roomSecretsObjectStore4.createIndex('display_name', 'display_name');\n                roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept');\n            }\n            if (e.oldVersion <= 4) {\n                // migrate to v5\n                const editedDisplayNameOld = await PersistentStorage.get('editedDisplayName');\n                if (editedDisplayNameOld) {\n                    await PersistentStorage.set('edited_display_name', editedDisplayNameOld);\n                    await PersistentStorage.delete('editedDisplayName');\n                }\n            }\n        }\n    }\n\n    static logBrowserNotCapable() {\n        console.log(\"This browser does not support IndexedDB. Paired devices will be gone after the browser is closed.\");\n    }\n\n    static set(key, value) {\n        return new Promise((resolve, reject) => {\n            const DBOpenRequest = window.indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = e => {\n                const db = e.target.result;\n                const transaction = db.transaction('keyval', 'readwrite');\n                const objectStore = transaction.objectStore('keyval');\n                const objectStoreRequest = objectStore.put(value, key);\n                objectStoreRequest.onsuccess = _ => {\n                    console.log(`Request successful. Added key-pair: ${key} - ${value}`);\n                    resolve(value);\n                };\n            }\n            DBOpenRequest.onerror = e => {\n                reject(e);\n            }\n        })\n    }\n\n    static get(key) {\n        return new Promise((resolve, reject) => {\n            const DBOpenRequest = window.indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = e => {\n                const db = e.target.result;\n                const transaction = db.transaction('keyval', 'readonly');\n                const objectStore = transaction.objectStore('keyval');\n                const objectStoreRequest = objectStore.get(key);\n                objectStoreRequest.onsuccess = _ => {\n                    console.log(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`);\n                    resolve(objectStoreRequest.result);\n                }\n            }\n            DBOpenRequest.onerror = e => {\n                reject(e);\n            }\n        });\n    }\n\n    static delete(key) {\n        return new Promise((resolve, reject) => {\n            const DBOpenRequest = window.indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = e => {\n                const db = e.target.result;\n                const transaction = db.transaction('keyval', 'readwrite');\n                const objectStore = transaction.objectStore('keyval');\n                const objectStoreRequest = objectStore.delete(key);\n                objectStoreRequest.onsuccess = _ => {\n                    console.log(`Request successful. Deleted key: ${key}`);\n                    resolve();\n                };\n            }\n            DBOpenRequest.onerror = e => {\n                reject(e);\n            }\n        })\n    }\n\n    static addRoomSecret(roomSecret, displayName, deviceName) {\n        return new Promise((resolve, reject) => {\n            const DBOpenRequest = window.indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = e => {\n                const db = e.target.result;\n                const transaction = db.transaction('room_secrets', 'readwrite');\n                const objectStore = transaction.objectStore('room_secrets');\n                const objectStoreRequest = objectStore.add({\n                    'secret': roomSecret,\n                    'display_name': displayName,\n                    'device_name': deviceName,\n                    'auto_accept': false\n                });\n                objectStoreRequest.onsuccess = e => {\n                    console.log(`Request successful. RoomSecret added: ${e.target.result}`);\n                    resolve();\n                }\n            }\n            DBOpenRequest.onerror = e => {\n                reject(e);\n            }\n        })\n    }\n\n    static async getAllRoomSecrets() {\n        try {\n            const roomSecrets = await this.getAllRoomSecretEntries();\n            let secrets = [];\n            for (let i = 0; i < roomSecrets.length; i++) {\n                secrets.push(roomSecrets[i].secret);\n            }\n            console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);\n            return(secrets);\n        } catch (e) {\n            this.logBrowserNotCapable();\n            return [];\n        }\n    }\n\n    static getAllRoomSecretEntries() {\n        return new Promise((resolve, reject) => {\n            const DBOpenRequest = window.indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = (e) => {\n                const db = e.target.result;\n                const transaction = db.transaction('room_secrets', 'readonly');\n                const objectStore = transaction.objectStore('room_secrets');\n                const objectStoreRequest = objectStore.getAll();\n                objectStoreRequest.onsuccess = e => {\n                    resolve(e.target.result);\n                }\n            }\n            DBOpenRequest.onerror = (e) => {\n                reject(e);\n            }\n        });\n    }\n\n    static getRoomSecretEntry(roomSecret) {\n        return new Promise((resolve, reject) => {\n            const DBOpenRequest = window.indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = e => {\n                const db = e.target.result;\n                const transaction = db.transaction('room_secrets', 'readonly');\n                const objectStore = transaction.objectStore('room_secrets');\n                const objectStoreRequestKey = objectStore.index(\"secret\").getKey(roomSecret);\n                objectStoreRequestKey.onsuccess = e => {\n                    const key = e.target.result;\n                    if (!key) {\n                        console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);\n                        resolve();\n                        return;\n                    }\n                    const objectStoreRequestRetrieval = objectStore.get(key);\n                    objectStoreRequestRetrieval.onsuccess = e => {\n                        console.log(`Request successful. Retrieved entry for room_secret: ${key}`);\n                        resolve({\n                            \"entry\": e.target.result,\n                            \"key\": key\n                        });\n                    }\n                    objectStoreRequestRetrieval.onerror = (e) => {\n                        reject(e);\n                    }\n                };\n            }\n            DBOpenRequest.onerror = (e) => {\n                reject(e);\n            }\n        });\n    }\n\n    static deleteRoomSecret(roomSecret) {\n        return new Promise((resolve, reject) => {\n            const DBOpenRequest = window.indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = (e) => {\n                const db = e.target.result;\n                const transaction = db.transaction('room_secrets', 'readwrite');\n                const objectStore = transaction.objectStore('room_secrets');\n                const objectStoreRequestKey = objectStore.index(\"secret\").getKey(roomSecret);\n                objectStoreRequestKey.onsuccess = e => {\n                    if (!e.target.result) {\n                        console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);\n                        resolve();\n                        return;\n                    }\n                    const key = e.target.result;\n                    const objectStoreRequestDeletion = objectStore.delete(key);\n                    objectStoreRequestDeletion.onsuccess = _ => {\n                        console.log(`Request successful. Deleted room_secret: ${key}`);\n                        resolve(roomSecret);\n                    }\n                    objectStoreRequestDeletion.onerror = e => {\n                        reject(e);\n                    }\n                };\n            }\n            DBOpenRequest.onerror = e => {\n                reject(e);\n            }\n        })\n    }\n\n    static clearRoomSecrets() {\n        return new Promise((resolve, reject) => {\n            const DBOpenRequest = window.indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = (e) => {\n                const db = e.target.result;\n                const transaction = db.transaction('room_secrets', 'readwrite');\n                const objectStore = transaction.objectStore('room_secrets');\n                const objectStoreRequest = objectStore.clear();\n                objectStoreRequest.onsuccess = _ => {\n                    console.log('Request successful. All room_secrets cleared');\n                    resolve();\n                };\n            }\n            DBOpenRequest.onerror = e => {\n                reject(e);\n            }\n        })\n    }\n\n    static updateRoomSecretNames(roomSecret, displayName, deviceName) {\n        return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);\n    }\n\n    static updateRoomSecretAutoAccept(roomSecret, autoAccept) {\n        return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);\n    }\n\n    static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {\n        return new Promise((resolve, reject) => {\n            const DBOpenRequest = window.indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = e => {\n                const db = e.target.result;\n                this.getRoomSecretEntry(roomSecret)\n                    .then(roomSecretEntry => {\n                        if (!roomSecretEntry) {\n                            resolve(false);\n                            return;\n                        }\n                        const transaction = db.transaction('room_secrets', 'readwrite');\n                        const objectStore = transaction.objectStore('room_secrets');\n                        // Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers\n                        const updatedRoomSecretEntry = {\n                            'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,\n                            'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,\n                            'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,\n                            'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept\n                        };\n\n                        const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);\n\n                        objectStoreRequestUpdate.onsuccess = e => {\n                            console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);\n                            resolve({\n                                \"entry\": updatedRoomSecretEntry,\n                                \"key\": roomSecretEntry.key\n                            });\n                        }\n\n                        objectStoreRequestUpdate.onerror = (e) => {\n                            reject(e);\n                        }\n                    })\n                    .catch(e => reject(e));\n            };\n\n            DBOpenRequest.onerror = e => reject(e);\n        })\n    }\n}"
  },
  {
    "path": "public/scripts/ui-main.js",
    "content": "// Selector shortcuts\nconst $ = query => document.getElementById(query);\nconst $$ = query => document.querySelector(query);\n\n// Event listener shortcuts\nclass Events {\n    static fire(type, detail = {}) {\n        window.dispatchEvent(new CustomEvent(type, { detail: detail }));\n    }\n\n    static on(type, callback, options) {\n        return window.addEventListener(type, callback, options);\n    }\n\n    static off(type, callback, options) {\n        return window.removeEventListener(type, callback, options);\n    }\n}\n\n// UIs needed on start\nclass ThemeUI {\n\n    constructor() {\n        this.prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;\n        this.prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;\n\n        this.$themeAutoBtn = document.getElementById('theme-auto');\n        this.$themeLightBtn = document.getElementById('theme-light');\n        this.$themeDarkBtn = document.getElementById('theme-dark');\n\n        let currentTheme = this.getCurrentTheme();\n        if (currentTheme === 'dark') {\n            this.setModeToDark();\n        } else if (currentTheme === 'light') {\n            this.setModeToLight();\n        }\n\n        this.$themeAutoBtn.addEventListener('click', _ => this.onClickAuto());\n        this.$themeLightBtn.addEventListener('click', _ => this.onClickLight());\n        this.$themeDarkBtn.addEventListener('click', _ => this.onClickDark());\n    }\n\n    getCurrentTheme() {\n        return localStorage.getItem('theme');\n    }\n\n    setCurrentTheme(theme) {\n        localStorage.setItem('theme', theme);\n    }\n\n    onClickAuto() {\n        if (this.getCurrentTheme()) {\n            this.setModeToAuto();\n        } else {\n            this.setModeToDark();\n        }\n    }\n\n    onClickLight() {\n        if (this.getCurrentTheme() !== 'light') {\n            this.setModeToLight();\n        } else {\n            this.setModeToAuto();\n        }\n    }\n\n    onClickDark() {\n        if (this.getCurrentTheme() !== 'dark') {\n            this.setModeToDark();\n        } else {\n            this.setModeToLight();\n        }\n    }\n\n    setModeToDark() {\n        document.body.classList.remove('light-theme');\n        document.body.classList.add('dark-theme');\n\n        this.setCurrentTheme('dark');\n\n        this.$themeAutoBtn.classList.remove(\"selected\");\n        this.$themeLightBtn.classList.remove(\"selected\");\n        this.$themeDarkBtn.classList.add(\"selected\");\n    }\n\n    setModeToLight() {\n        document.body.classList.remove('dark-theme');\n        document.body.classList.add('light-theme');\n\n        this.setCurrentTheme('light');\n\n        this.$themeAutoBtn.classList.remove(\"selected\");\n        this.$themeLightBtn.classList.add(\"selected\");\n        this.$themeDarkBtn.classList.remove(\"selected\");\n    }\n\n    setModeToAuto() {\n        document.body.classList.remove('dark-theme');\n        document.body.classList.remove('light-theme');\n        if (this.prefersDarkTheme) {\n            document.body.classList.add('dark-theme');\n        }\n        else if (this.prefersLightTheme) {\n            document.body.classList.add('light-theme');\n        }\n        localStorage.removeItem('theme');\n\n        this.$themeAutoBtn.classList.add(\"selected\");\n        this.$themeLightBtn.classList.remove(\"selected\");\n        this.$themeDarkBtn.classList.remove(\"selected\");\n    }\n}\n\nclass HeaderUI {\n\n    constructor() {\n        this.$header = $$('header');\n        this.$expandBtn = $('expand');\n        Events.on(\"resize\", _ => this.evaluateOverflowing());\n        this.$expandBtn.addEventListener('click', _ => this.onExpandBtnClick());\n    }\n\n    async fadeIn() {\n        this.$header.classList.remove('opacity-0');\n    }\n\n    async evaluateOverflowing() {\n        // remove bracket icon before evaluating\n        this.$expandBtn.setAttribute('hidden', true);\n        // reset bracket icon rotation and header overflow\n        this.$expandBtn.classList.add('flipped');\n        this.$header.classList.remove('overflow-expanded');\n\n\n        const rtlLocale = Localization.currentLocaleIsRtl();\n        let icon;\n        const $headerIconsShown = document.querySelectorAll('body > header:first-of-type > *:not([hidden])');\n\n        for (let i= 1; i < $headerIconsShown.length; i++) {\n            let isFurtherLeftThanLastIcon = $headerIconsShown[i].offsetLeft >= $headerIconsShown[i-1].offsetLeft;\n            let isFurtherRightThanLastIcon = $headerIconsShown[i].offsetLeft <= $headerIconsShown[i-1].offsetLeft;\n            if ((!rtlLocale && isFurtherLeftThanLastIcon) || (rtlLocale && isFurtherRightThanLastIcon)) {\n                // we have found the first icon on second row. Use previous icon.\n                icon = $headerIconsShown[i-1];\n                break;\n            }\n        }\n        if (icon) {\n            // overflowing\n            // add overflowing-hidden class\n            this.$header.classList.add('overflow-hidden');\n            // add expand btn 2 before icon\n            this.$expandBtn.removeAttribute('hidden');\n            icon.before(this.$expandBtn);\n        }\n        else {\n            // no overflowing\n            // remove overflowing-hidden class\n            this.$header.classList.remove('overflow-hidden');\n        }\n    }\n\n    onExpandBtnClick() {\n        // toggle overflowing-hidden class and flip expand btn icon\n        if (this.$header.classList.contains('overflow-hidden')) {\n            this.$header.classList.remove('overflow-hidden');\n            this.$header.classList.add('overflow-expanded');\n            this.$expandBtn.classList.remove('flipped');\n        }\n        else {\n            this.$header.classList.add('overflow-hidden');\n            this.$header.classList.remove('overflow-expanded');\n            this.$expandBtn.classList.add('flipped');\n        }\n        Events.fire('header-changed');\n    }\n}\n\nclass CenterUI {\n\n    constructor() {\n        this.$center = $$('#center');\n        this.$xNoPeers = $$('x-no-peers');\n    }\n\n    async fadeIn() {\n        this.$center.classList.remove('opacity-0');\n\n        // Prevent flickering on load\n        setTimeout(() => {\n            this.$xNoPeers.classList.remove('no-animation-on-load');\n        }, 600);\n    }\n}\n\nclass FooterUI {\n\n    constructor() {\n        this.$footer = $$('footer');\n        this.$displayName = $('display-name');\n        this.$discoveryWrapper = $$('footer .discovery-wrapper');\n\n        this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));\n        this.$displayName.addEventListener('focus', e => this._onFocusDisplayName(e));\n        this.$displayName.addEventListener('blur', e => this._onBlurDisplayName(e));\n\n        Events.on('display-name', e => this._onDisplayName(e.detail.displayName));\n        Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));\n        Events.on('evaluate-footer-badges', _ => this._evaluateFooterBadges());\n    }\n\n    async showLoading() {\n        this.$displayName.setAttribute('placeholder', this.$displayName.dataset.placeholder);\n    }\n\n    async fadeIn() {\n        this.$footer.classList.remove('opacity-0');\n    }\n\n    async _evaluateFooterBadges() {\n        if (this.$discoveryWrapper.querySelectorAll('div:last-of-type > span[hidden]').length < 2) {\n            this.$discoveryWrapper.classList.remove('row');\n            this.$discoveryWrapper.classList.add('column');\n        }\n        else {\n            this.$discoveryWrapper.classList.remove('column');\n            this.$discoveryWrapper.classList.add('row');\n        }\n        Events.fire('redraw-canvas');\n    }\n\n    async _loadSavedDisplayName() {\n        const displayNameSaved = await this._getSavedDisplayName()\n\n        if (!displayNameSaved) return;\n\n        console.log(\"Retrieved edited display name:\", displayNameSaved)\n        Events.fire('self-display-name-changed', displayNameSaved);\n    }\n\n    async _onDisplayName(displayNameServer){\n        // load saved displayname first to prevent flickering\n        await this._loadSavedDisplayName();\n\n        // set original display name as placeholder\n        this.$displayName.setAttribute('placeholder', displayNameServer);\n    }\n\n\n    _insertDisplayName(displayName) {\n        this.$displayName.textContent = displayName;\n    }\n\n    _onKeyDownDisplayName(e) {\n        if (e.key === \"Enter\" || e.key === \"Escape\") {\n            e.preventDefault();\n            e.target.blur();\n        }\n    }\n\n    _onFocusDisplayName(e) {\n        if (!e.target.innerText) {\n            // Fix z-position of cursor when div is completely empty (Firefox only)\n            e.target.innerText = \"\\n\";\n\n            // On Chromium based browsers the cursor position is lost when adding sth. to the focused node. This adds it back.\n            let sel = window.getSelection();\n            sel.collapse(e.target.lastChild);\n        }\n    }\n\n    async _onBlurDisplayName(e) {\n        // fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty\n        if (/^(\\n|\\r|\\r\\n)$/.test(e.target.innerText)) {\n            e.target.innerText = '';\n        }\n\n        // Remove selection from text\n        window.getSelection().removeAllRanges();\n\n        await this._saveDisplayName(e.target.innerText)\n    }\n\n    async _saveDisplayName(newDisplayName) {\n        newDisplayName = newDisplayName.replace(/(\\n|\\r|\\r\\n)/, '')\n        const savedDisplayName = await this._getSavedDisplayName();\n        if (newDisplayName === savedDisplayName) return;\n\n        if (newDisplayName) {\n            PersistentStorage.set('edited_display_name', newDisplayName)\n                .then(_ => {\n                    Events.fire('notify-user', Localization.getTranslation(\"notifications.display-name-changed-permanently\"));\n                })\n                .catch(_ => {\n                    console.log(\"This browser does not support IndexedDB. Use localStorage instead.\");\n                    localStorage.setItem('edited_display_name', newDisplayName);\n                    Events.fire('notify-user', Localization.getTranslation(\"notifications.display-name-changed-temporarily\"));\n                })\n                .finally(() => {\n                    Events.fire('self-display-name-changed', newDisplayName);\n                    Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});\n                });\n        }\n        else {\n            PersistentStorage.delete('edited_display_name')\n                .catch(_ => {\n                    console.log(\"This browser does not support IndexedDB. Use localStorage instead.\")\n                    localStorage.removeItem('edited_display_name');\n                })\n                .finally(() => {\n                    Events.fire('notify-user', Localization.getTranslation(\"notifications.display-name-random-again\"));\n                    Events.fire('self-display-name-changed', '');\n                    Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});\n                });\n        }\n    }\n\n    _getSavedDisplayName() {\n        return new Promise((resolve) => {\n            PersistentStorage.get('edited_display_name')\n                .then(displayName => {\n                    if (!displayName) displayName = \"\";\n                    resolve(displayName);\n                })\n                .catch(_ => {\n                    let displayName = localStorage.getItem('edited_display_name');\n                    if (!displayName) displayName = \"\";\n                    resolve(displayName);\n                })\n        });\n    }\n}\n\nclass BackgroundCanvas {\n    constructor() {\n        this.$canvas = $$('canvas');\n        this.$footer = $$('footer');\n\n        this.initAnimation();\n    }\n\n    async fadeIn() {\n        this.$canvas.classList.remove('opacity-0');\n    }\n\n    initAnimation() {\n        this.baseColorNormal = '168 168 168';\n        this.baseColorShareMode = '168 168 255';\n        this.baseOpacityNormal = 0.3;\n        this.baseOpacityShareMode = 0.8;\n        this.speed = 0.5;\n        this.fps = 60;\n\n        // if browser supports OffscreenCanvas\n        //      -> put canvas drawing into serviceworker to unblock main thread\n        // otherwise\n        //      -> use main thread\n        let {init, startAnimation, switchAnimation, onShareModeChange} =\n            this.$canvas.transferControlToOffscreen\n                ? this.initAnimationOffscreen()\n                : this.initAnimationOnscreen();\n\n        init();\n        startAnimation();\n\n        // redraw canvas\n        Events.on('resize', _ => init());\n        Events.on('redraw-canvas', _ => init());\n        Events.on('translation-loaded', _ => init());\n\n        // ShareMode\n        Events.on('share-mode-changed', e => onShareModeChange(e.detail.active));\n\n        // Start and stop animation\n        Events.on('background-animation', e => switchAnimation(e.detail.animate))\n        Events.on('offline', _ => switchAnimation(false));\n        Events.on('online', _ => switchAnimation(true));\n    }\n\n    initAnimationOnscreen() {\n        let $canvas = this.$canvas;\n        let $footer = this.$footer;\n\n        let baseColorNormal = this.baseColorNormal;\n        let baseColorShareMode = this.baseColorShareMode;\n        let baseOpacityNormal = this.baseOpacityNormal;\n        let baseOpacityShareMode = this.baseOpacityShareMode;\n        let speed = this.speed;\n        let fps = this.fps;\n\n        let c;\n        let cCtx;\n\n        let x0, y0, w, h, dw, offset;\n\n        let startTime;\n        let animate = true;\n        let currentFrame = 0;\n        let lastFrame;\n        let baseColor;\n        let baseOpacity;\n\n        function createCanvas() {\n            c = $canvas;\n            cCtx = c.getContext('2d');\n\n            lastFrame = fps / speed - 1;\n            baseColor = baseColorNormal;\n            baseOpacity = baseOpacityNormal;\n        }\n\n        function init() {\n            initCanvas($footer.offsetHeight, document.documentElement.clientWidth, document.documentElement.clientHeight);\n        }\n\n        function initCanvas(footerOffsetHeight, clientWidth, clientHeight) {\n            let oldW = w;\n            let oldH = h;\n            let oldOffset = offset;\n            w = clientWidth;\n            h = clientHeight;\n            offset = footerOffsetHeight - 28;\n\n            if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed\n\n            c.width = w;\n            c.height = h;\n            x0 = w / 2;\n            y0 = h - offset;\n            dw = Math.round(Math.min(Math.max(0.6 * w, h)) / 10);\n\n            drawFrame(currentFrame);\n        }\n\n        function startAnimation() {\n            startTime = Date.now();\n            animateBg();\n        }\n\n        function switchAnimation(state) {\n            if (!animate && state) {\n                // animation starts again. Set startTime to specific value to prevent frame jump\n                startTime = Date.now() - 1000 * currentFrame / fps;\n            }\n            animate = state;\n            requestAnimationFrame(animateBg);\n        }\n\n        function onShareModeChange(active) {\n            baseColor = active ? baseColorShareMode : baseColorNormal;\n            baseOpacity = active ? baseOpacityShareMode : baseOpacityNormal;\n            drawFrame(currentFrame);\n        }\n\n        function drawCircle(ctx, radius) {\n            ctx.lineWidth = 2;\n\n            let opacity = Math.max(0, baseOpacity * (1 - 1.2 * radius / Math.max(w, h)));\n            if (radius > dw * 7) {\n                opacity *= (8 * dw - radius) / dw\n            }\n\n            if (ctx.setStrokeColor) {\n                // older blink/webkit browsers do not understand opacity in strokeStyle. Use deprecated setStrokeColor\n                // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle#webkitblink-specific_note\n                ctx.setStrokeColor(\"grey\", opacity);\n            }\n            else {\n                ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`;\n            }\n            ctx.beginPath();\n            ctx.arc(x0, y0, radius, 0, 2 * Math.PI);\n            ctx.stroke();\n        }\n\n        function drawCircles(ctx, frame) {\n            ctx.clearRect(0, 0, w, h);\n            for (let i = 7; i >= 0; i--) {\n                drawCircle(ctx, dw * i + speed * dw * frame / fps + 33);\n            }\n        }\n\n        function drawFrame(frame) {\n            cCtx.clearRect(0, 0, w, h);\n            drawCircles(cCtx, frame);\n        }\n\n        function animateBg() {\n            let now = Date.now();\n\n            if (!animate && currentFrame === lastFrame) {\n                // Animation stopped and cycle finished -> stop drawing frames\n                return;\n            }\n\n            let timeSinceLastFullCycle = (now - startTime) % (1000 / speed);\n            let nextFrame = Math.trunc(fps * timeSinceLastFullCycle / 1000);\n\n            // Only draw frame if it differs from current frame\n            if (nextFrame !== currentFrame) {\n                drawFrame(nextFrame);\n                currentFrame = nextFrame;\n            }\n\n            requestAnimationFrame(animateBg);\n        }\n\n        createCanvas();\n\n        return {init, startAnimation, switchAnimation, onShareModeChange};\n    }\n\n    initAnimationOffscreen() {\n        console.log(\"Use OffscreenCanvas to draw background animation.\")\n\n        let baseColorNormal = this.baseColorNormal;\n        let baseColorShareMode = this.baseColorShareMode;\n        let baseOpacityNormal = this.baseOpacityNormal;\n        let baseOpacityShareMode = this.baseOpacityShareMode;\n        let speed = this.speed;\n        let fps = this.fps;\n        let $canvas = this.$canvas;\n        let $footer = this.$footer;\n\n        const offscreen = $canvas.transferControlToOffscreen();\n        const worker = new Worker(\"scripts/worker/canvas-worker.js\");\n\n        function createCanvas() {\n            worker.postMessage({\n                type: \"createCanvas\",\n                canvas: offscreen,\n                baseColorNormal: baseColorNormal,\n                baseColorShareMode: baseColorShareMode,\n                baseOpacityNormal: baseOpacityNormal,\n                baseOpacityShareMode: baseOpacityShareMode,\n                speed: speed,\n                fps: fps\n            }, [offscreen]);\n        }\n\n        function init() {\n            worker.postMessage({\n                type: \"initCanvas\",\n                footerOffsetHeight: $footer.offsetHeight,\n                clientWidth: document.documentElement.clientWidth,\n                clientHeight: document.documentElement.clientHeight\n            });\n        }\n\n        function startAnimation() {\n            worker.postMessage({ type: \"startAnimation\" });\n        }\n\n        function onShareModeChange(active) {\n            worker.postMessage({ type: \"onShareModeChange\", active: active });\n        }\n\n        function switchAnimation(animate) {\n            worker.postMessage({ type: \"switchAnimation\", animate: animate });\n        }\n\n        createCanvas();\n\n        return {init, startAnimation, switchAnimation, onShareModeChange};\n    }\n}"
  },
  {
    "path": "public/scripts/ui.js",
    "content": "class PeersUI {\n\n    constructor() {\n        this.$xPeers = $$('x-peers');\n        this.$xNoPeers = $$('x-no-peers');\n        this.$xInstructions = $$('x-instructions');\n        this.$wsFallbackWarning = $('websocket-fallback');\n\n        this.$sharePanel = $$('.shr-panel');\n        this.$shareModeImageThumb = $$('.shr-panel .image-thumb');\n        this.$shareModeTextThumb = $$('.shr-panel .text-thumb');\n        this.$shareModeFileThumb = $$('.shr-panel .file-thumb');\n        this.$shareModeDescriptor = $$('.shr-panel .share-descriptor');\n        this.$shareModeDescriptorItem = $$('.shr-panel .descriptor-item');\n        this.$shareModeDescriptorOther = $$('.shr-panel .descriptor-other');\n        this.$shareModeCancelBtn = $$('.shr-panel .cancel-btn');\n        this.$shareModeEditBtn = $$('.shr-panel .edit-btn');\n\n        this.peers = {};\n\n        this.shareMode = {\n            active: false,\n            descriptor: \"\",\n            files: [],\n            text: \"\"\n        }\n\n        Events.on('peer-joined', e => this._onPeerJoined(e.detail));\n        Events.on('peer-added', _ => this._evaluateOverflowingPeers());\n        Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash));\n        Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));\n        Events.on('peers', e => this._onPeers(e.detail));\n        Events.on('set-progress', e => this._onSetProgress(e.detail));\n\n        Events.on('drop', e => this._onDrop(e));\n        Events.on('keydown', e => this._onKeyDown(e));\n        Events.on('dragover', e => this._onDragOver(e));\n        Events.on('dragleave', _ => this._onDragEnd());\n        Events.on('dragend', _ => this._onDragEnd());\n        Events.on('resize', _ => this._evaluateOverflowingPeers());\n        Events.on('header-changed', _ => this._evaluateOverflowingPeers());\n\n        Events.on('paste', e => this._onPaste(e));\n        Events.on('activate-share-mode', e => this._activateShareMode(e.detail.files, e.detail.text));\n        Events.on('translation-loaded', _ => this._reloadShareMode());\n        Events.on('room-type-removed', e => this._onRoomTypeRemoved(e.detail.peerId, e.detail.roomType));\n\n\n        this.$shareModeCancelBtn.addEventListener('click', _ => this._deactivateShareMode());\n\n        Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));\n\n        Events.on('ws-config', e => this._evaluateRtcSupport(e.detail))\n    }\n\n    _evaluateRtcSupport(wsConfig) {\n        if (wsConfig.wsFallback) {\n            this.$wsFallbackWarning.hidden = false;\n        }\n        else {\n            this.$wsFallbackWarning.hidden = true;\n            if (!window.isRtcSupported) {\n                alert(Localization.getTranslation(\"instructions.webrtc-requirement\"));\n            }\n        }\n    }\n\n    _changePeerDisplayName(peerId, displayName) {\n        this.peers[peerId].name.displayName = displayName;\n        const peerIdNode = $(peerId);\n        if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;\n        this._redrawPeerRoomTypes(peerId);\n    }\n\n    _onPeerDisplayNameChanged(e) {\n        if (!e.detail.displayName) return;\n        this._changePeerDisplayName(e.detail.peerId, e.detail.displayName);\n    }\n\n    async _onKeyDown(e) {\n        if (!this.shareMode.active || Dialog.anyDialogShown()) return;\n\n        if (e.key === \"Escape\") {\n            await this._deactivateShareMode();\n        }\n\n        // close About PairDrop page on Escape\n        if (e.key === \"Escape\") {\n            window.location.hash = '#';\n        }\n    }\n\n    _onPeerJoined(msg) {\n        this._joinPeer(msg.peer, msg.roomType, msg.roomId);\n    }\n\n    _joinPeer(peer, roomType, roomId) {\n        const existingPeer = this.peers[peer.id];\n        if (existingPeer) {\n            // peer already exists. Abort but add roomType to GUI\n            existingPeer._roomIds[roomType] = roomId;\n            this._redrawPeerRoomTypes(peer.id);\n            return;\n        }\n\n        peer._isSameBrowser = () => BrowserTabsConnector.peerIsSameBrowser(peer.id);\n        peer._roomIds = {};\n\n        peer._roomIds[roomType] = roomId;\n        this.peers[peer.id] = peer;\n    }\n\n    _onPeerConnected(peerId, connectionHash) {\n        if (!this.peers[peerId] || $(peerId)) return;\n\n        const peer = this.peers[peerId];\n\n        new PeerUI(peer, connectionHash, {\n            active: this.shareMode.active,\n            descriptor: this.shareMode.descriptor,\n        });\n    }\n\n    _redrawPeerRoomTypes(peerId) {\n        const peer = this.peers[peerId];\n        const peerNode = $(peerId);\n\n        if (!peer || !peerNode) return;\n\n        peerNode.classList.remove('type-ip', 'type-secret', 'type-public-id', 'type-same-browser');\n\n        if (peer._isSameBrowser()) {\n            peerNode.classList.add(`type-same-browser`);\n        }\n\n        Object.keys(peer._roomIds).forEach(roomType => peerNode.classList.add(`type-${roomType}`));\n    }\n\n    _evaluateOverflowingPeers() {\n        if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) {\n            this.$xPeers.classList.add('overflowing');\n        }\n        else {\n            this.$xPeers.classList.remove('overflowing');\n        }\n    }\n\n    _onPeers(msg) {\n        msg.peers.forEach(peer => this._joinPeer(peer, msg.roomType, msg.roomId));\n    }\n\n    _onPeerDisconnected(peerId) {\n        // Remove peer from UI\n        const $peer = $(peerId);\n        if (!$peer) return;\n        $peer.remove();\n        this._evaluateOverflowingPeers();\n\n        // If no peer is shown -> start background animation again\n        if ($$('x-peers:empty')) {\n            Events.fire('background-animation', {animate: true});\n        }\n\n    }\n\n    _onRoomTypeRemoved(peerId, roomType) {\n        const peer = this.peers[peerId];\n\n        if (!peer) return;\n\n        delete peer._roomIds[roomType];\n\n        this._redrawPeerRoomTypes(peerId)\n    }\n\n    _onSetProgress(progress) {\n        const $peer = $(progress.peerId);\n        if (!$peer) return;\n        $peer.ui.setProgress(progress.progress, progress.status)\n    }\n\n    _onDrop(e) {\n        if (this.shareMode.active || Dialog.anyDialogShown()) return;\n\n        e.preventDefault();\n\n        this._onDragEnd();\n\n        if ($$('x-peer') && $$('x-peer').contains(e.target)) return; // dropped on peer\n\n        let files = e.dataTransfer.files;\n        let text = e.dataTransfer.getData(\"text\");\n\n        // convert FileList to Array\n        files = [...files];\n\n        if (files.length > 0) {\n            Events.fire('activate-share-mode', {\n                files: files\n            });\n        }\n        else if(text.length > 0) {\n            Events.fire('activate-share-mode', {\n                text: text\n            });\n        }\n    }\n\n    _onDragOver(e) {\n        if (this.shareMode.active || Dialog.anyDialogShown()) return;\n\n        e.preventDefault();\n\n        this.$xInstructions.setAttribute('drop-bg', true);\n        this.$xNoPeers.setAttribute('drop-bg', true);\n    }\n\n    _onDragEnd() {\n        this.$xInstructions.removeAttribute('drop-bg');\n        this.$xNoPeers.removeAttribute('drop-bg');\n    }\n\n    _onPaste(e) {\n        // prevent send on paste when dialog is open\n        if (this.shareMode.active || Dialog.anyDialogShown()) return;\n\n        e.preventDefault()\n        let files = e.clipboardData.files;\n        let text = e.clipboardData.getData(\"Text\");\n\n        // convert FileList to Array\n        files = [...files];\n\n        if (files.length > 0) {\n            Events.fire('activate-share-mode', {files: files});\n        } else if (text.length > 0) {\n            if (ShareTextDialog.isApproveShareTextSet()) {\n                Events.fire('share-text-dialog', text);\n            } else {\n                Events.fire('activate-share-mode', {text: text});\n            }\n        }\n    }\n\n    async _activateShareMode(files = [], text = \"\") {\n        if (this.shareMode.active || (files.length === 0 && text.length === 0)) return;\n\n        this._activateCallback = e => this._sendShareData(e);\n        this._editShareTextCallback = _ => {\n            this._deactivateShareMode();\n            Events.fire('share-text-dialog', text);\n        };\n\n        Events.on('share-mode-pointerdown', this._activateCallback);\n\n        const sharedText = Localization.getTranslation(\"instructions.activate-share-mode-shared-text\");\n        const andOtherFilesPlural = Localization.getTranslation(\"instructions.activate-share-mode-and-other-files-plural\", null, {count: files.length-1});\n        const andOtherFiles = Localization.getTranslation(\"instructions.activate-share-mode-and-other-file\");\n\n        let descriptorComplete, descriptorItem, descriptorOther, descriptorInstructions;\n\n        if (files.length > 2) {\n            // files shared\n            descriptorItem = files[0].name;\n            descriptorOther = andOtherFilesPlural;\n            descriptorComplete = `${descriptorItem} ${descriptorOther}`;\n        }\n        else if (files.length === 2) {\n            descriptorItem = files[0].name;\n            descriptorOther = andOtherFiles;\n            descriptorComplete = `${descriptorItem} ${descriptorOther}`;\n        } else if (files.length === 1) {\n            descriptorItem = files[0].name;\n            descriptorComplete = descriptorItem;\n        }\n        else {\n            // text shared\n            descriptorItem = text.replace(/\\s/g,\" \");\n            descriptorComplete = sharedText;\n        }\n\n        if (files.length > 0) {\n            if (descriptorOther) {\n                this.$shareModeDescriptorOther.innerText = descriptorOther;\n                this.$shareModeDescriptorOther.removeAttribute('hidden');\n            }\n            if (files.length > 1) {\n                descriptorInstructions = Localization.getTranslation(\"instructions.activate-share-mode-shared-files-plural\", null, {count: files.length});\n            }\n            else {\n                descriptorInstructions = Localization.getTranslation(\"instructions.activate-share-mode-shared-file\");\n            }\n\n            if (files[0].type.split('/')[0] === 'image') {\n                try {\n                    let imageUrl = await getThumbnailAsDataUrl(files[0], 80, null, 0.9);\n\n                    this.$shareModeImageThumb.style.backgroundImage = `url(${imageUrl})`;\n\n                    this.$shareModeImageThumb.removeAttribute('hidden');\n                } catch (e) {\n                    console.error(e);\n                    this.$shareModeFileThumb.removeAttribute('hidden');\n                }\n            } else {\n                this.$shareModeFileThumb.removeAttribute('hidden');\n            }\n        }\n        else {\n            this.$shareModeTextThumb.removeAttribute('hidden');\n\n            this.$shareModeEditBtn.addEventListener('click', this._editShareTextCallback);\n            this.$shareModeEditBtn.removeAttribute('hidden');\n\n            descriptorInstructions = Localization.getTranslation(\"instructions.activate-share-mode-shared-text\");\n        }\n\n        const desktop = Localization.getTranslation(\"instructions.x-instructions-share-mode_desktop\", null, {descriptor: descriptorInstructions});\n        const mobile = Localization.getTranslation(\"instructions.x-instructions-share-mode_mobile\", null, {descriptor: descriptorInstructions});\n\n        this.$xInstructions.setAttribute('desktop', desktop);\n        this.$xInstructions.setAttribute('mobile', mobile);\n\n        this.$sharePanel.removeAttribute('hidden');\n\n        this.$shareModeDescriptor.removeAttribute('hidden');\n        this.$shareModeDescriptorItem.innerText = descriptorItem;\n\n        this.shareMode.active = true;\n        this.shareMode.descriptor = descriptorComplete;\n        this.shareMode.files = files;\n        this.shareMode.text = text;\n\n        console.log('Share mode activated.');\n\n        Events.fire('share-mode-changed', {\n            active: true,\n            descriptor: descriptorComplete\n        });\n    }\n\n    async _reloadShareMode() {\n        // If shareMode is active only\n        if (!this.shareMode.active) return;\n\n        let files = this.shareMode.files;\n        let text = this.shareMode.text;\n\n        await this._deactivateShareMode();\n        await this._activateShareMode(files, text);\n    }\n\n    async _deactivateShareMode() {\n        if (!this.shareMode.active) return;\n\n        this.shareMode.active = false;\n        this.shareMode.descriptor = \"\";\n        this.shareMode.files = [];\n        this.shareMode.text = \"\";\n\n        Events.off('share-mode-pointerdown', this._activateCallback);\n\n        const desktop = Localization.getTranslation(\"instructions.x-instructions_desktop\");\n        const mobile = Localization.getTranslation(\"instructions.x-instructions_mobile\");\n\n        this.$xInstructions.setAttribute('desktop', desktop);\n        this.$xInstructions.setAttribute('mobile', mobile);\n\n        this.$sharePanel.setAttribute('hidden', true);\n\n        this.$shareModeImageThumb.setAttribute('hidden', true);\n        this.$shareModeFileThumb.setAttribute('hidden', true);\n        this.$shareModeTextThumb.setAttribute('hidden', true);\n\n        this.$shareModeDescriptorItem.innerHTML = \"\";\n        this.$shareModeDescriptorItem.classList.remove('cursive');\n        this.$shareModeDescriptorOther.innerHTML = \"\";\n        this.$shareModeDescriptorOther.setAttribute('hidden', true);\n        this.$shareModeEditBtn.removeEventListener('click', this._editShareTextCallback);\n        this.$shareModeEditBtn.setAttribute('hidden', true);\n\n        console.log('Share mode deactivated.')\n        Events.fire('share-mode-changed', { active: false });\n    }\n\n    _sendShareData(e) {\n        // send the shared file/text content\n        const peerId = e.detail.peerId;\n        const files = this.shareMode.files;\n        const text = this.shareMode.text;\n\n        if (files.length > 0) {\n            Events.fire('files-selected', {\n                files: files,\n                to: peerId\n            });\n        }\n        else if (text.length > 0) {\n            Events.fire('send-text', {\n                text: text,\n                to: peerId\n            });\n        }\n    }\n}\n\nclass PeerUI {\n\n    constructor(peer, connectionHash, shareMode) {\n        this.$xInstructions = $$('x-instructions');\n        this.$xPeers = $$('x-peers');\n\n        this._peer = peer;\n        this._connectionHash =\n            `${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;\n\n        // This is needed if the ShareMode is started BEFORE the PeerUI is drawn.\n        this._shareMode = shareMode;\n\n        this._initDom();\n\n        this.$xPeers.appendChild(this.$el);\n        Events.fire('peer-added');\n\n        // ShareMode\n        Events.on('share-mode-changed', e => this._onShareModeChanged(e.detail.active, e.detail.descriptor));\n\n        // Stop background animation\n        Events.fire('background-animation', {animate: false});\n    }\n\n    html() {\n        let title= this._shareMode.active\n            ? Localization.getTranslation(\"peer-ui.click-to-send-share-mode\", null, {descriptor: this._shareMode.descriptor})\n            : Localization.getTranslation(\"peer-ui.click-to-send\");\n\n        this.$el.innerHTML = `\n            <label class=\"column center pointer\" title=\"${title}\">\n                <input type=\"file\" multiple/>\n                <x-icon>\n                    <div class=\"icon-wrapper\" shadow=\"1\">\n                        <svg class=\"icon\"><use xlink:href=\"#\"/></svg>\n                    </div>\n                    <div class=\"highlight-wrapper center\">\n                        <div class=\"highlight highlight-room-ip\" shadow=\"1\"></div>\n                        <div class=\"highlight highlight-room-secret\" shadow=\"1\"></div>\n                        <div class=\"highlight highlight-room-public-id\" shadow=\"1\"></div>\n                    </div>\n                </x-icon>\n                <div class=\"progress\">\n                  <div class=\"circle\"></div>\n                  <div class=\"circle right\"></div>\n                </div>\n                <div class=\"device-descriptor\">\n                    <div class=\"name font-subheading\"></div>\n                    <div class=\"device-name font-body2\"></div>\n                    <div class=\"status font-body2\"></div>\n                </div>\n            </label>`;\n\n        this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());\n        this.$el.querySelector('.name').textContent = this._displayName();\n        this.$el.querySelector('.device-name').textContent = this._deviceName();\n\n        this.$label = this.$el.querySelector('label');\n        this.$input = this.$el.querySelector('input');\n    }\n\n    addTypesToClassList() {\n        if (this._peer._isSameBrowser()) {\n            this.$el.classList.add(`type-same-browser`);\n        }\n\n        Object.keys(this._peer._roomIds).forEach(roomType => this.$el.classList.add(`type-${roomType}`));\n\n        if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer');\n    }\n\n    _initDom() {\n        this.$el = document.createElement('x-peer');\n        this.$el.id = this._peer.id;\n        this.$el.ui = this;\n        this.$el.classList.add('center');\n\n        this.addTypesToClassList();\n\n        this.html();\n\n        this._createCallbacks();\n\n        this._evaluateShareMode();\n        this._bindListeners();\n    }\n\n    _onShareModeChanged(active = false, descriptor = \"\") {\n        // This is needed if the ShareMode is started AFTER the PeerUI is drawn.\n        this._shareMode.active = active;\n        this._shareMode.descriptor = descriptor;\n\n        this._evaluateShareMode();\n        this._bindListeners();\n    }\n\n    _evaluateShareMode() {\n        let title;\n        if (!this._shareMode.active) {\n            title = Localization.getTranslation(\"peer-ui.click-to-send\");\n            this.$input.removeAttribute('disabled');\n        }\n        else {\n            title =  Localization.getTranslation(\"peer-ui.click-to-send-share-mode\", null, {descriptor: this._shareMode.descriptor});\n            this.$input.setAttribute('disabled', true);\n        }\n        this.$label.setAttribute('title', title);\n    }\n\n    _createCallbacks() {\n        this._callbackInput = e => this._onFilesSelected(e);\n        this._callbackClickSleep = _ => NoSleepUI.enable();\n        this._callbackTouchStartSleep = _ => NoSleepUI.enable();\n        this._callbackDrop = e => this._onDrop(e);\n        this._callbackDragEnd = e => this._onDragEnd(e);\n        this._callbackDragLeave = e => this._onDragEnd(e);\n        this._callbackDragOver = e => this._onDragOver(e);\n        this._callbackContextMenu = e => this._onRightClick(e);\n        this._callbackTouchStart = e => this._onTouchStart(e);\n        this._callbackTouchEnd = e => this._onTouchEnd(e);\n        this._callbackPointerDown = e => this._onPointerDown(e);\n    }\n\n    _bindListeners() {\n        if(!this._shareMode.active) {\n            // Remove Events Share mode\n            this.$el.removeEventListener('pointerdown', this._callbackPointerDown);\n\n            // Add Events Normal Mode\n            this.$el.querySelector('input').addEventListener('change', this._callbackInput);\n            this.$el.addEventListener('click', this._callbackClickSleep);\n            this.$el.addEventListener('touchstart', this._callbackTouchStartSleep);\n            this.$el.addEventListener('drop', this._callbackDrop);\n            this.$el.addEventListener('dragend', this._callbackDragEnd);\n            this.$el.addEventListener('dragleave', this._callbackDragLeave);\n            this.$el.addEventListener('dragover', this._callbackDragOver);\n            this.$el.addEventListener('contextmenu', this._callbackContextMenu);\n            this.$el.addEventListener('touchstart', this._callbackTouchStart);\n            this.$el.addEventListener('touchend', this._callbackTouchEnd);\n        }\n        else {\n            // Remove Events Normal Mode\n            this.$el.removeEventListener('click', this._callbackClickSleep);\n            this.$el.removeEventListener('touchstart', this._callbackTouchStartSleep);\n            this.$el.removeEventListener('drop', this._callbackDrop);\n            this.$el.removeEventListener('dragend', this._callbackDragEnd);\n            this.$el.removeEventListener('dragleave', this._callbackDragLeave);\n            this.$el.removeEventListener('dragover', this._callbackDragOver);\n            this.$el.removeEventListener('contextmenu', this._callbackContextMenu);\n            this.$el.removeEventListener('touchstart', this._callbackTouchStart);\n            this.$el.removeEventListener('touchend', this._callbackTouchEnd);\n\n            // Add Events Share mode\n            this.$el.addEventListener('pointerdown', this._callbackPointerDown);\n        }\n    }\n\n    _onPointerDown(e) {\n        // Prevents triggering of event twice on touch devices\n        e.stopPropagation();\n        e.preventDefault();\n        Events.fire('share-mode-pointerdown', {\n            peerId: this._peer.id\n        });\n    }\n\n    _displayName() {\n        return this._peer.name.displayName;\n    }\n\n    _deviceName() {\n        return this._peer.name.deviceName;\n    }\n\n    _badgeClassName() {\n        const roomTypes = Object.keys(this._peer._roomIds);\n        return roomTypes.includes('secret')\n            ? 'badge-room-secret'\n            : roomTypes.includes('ip')\n                ? 'badge-room-ip'\n                : 'badge-room-public-id';\n    }\n\n    _icon() {\n        const device = this._peer.name.device || this._peer.name;\n        if (device.type === 'mobile') {\n            return '#phone-iphone';\n        }\n        if (device.type === 'tablet') {\n            return '#tablet-mac';\n        }\n        return '#desktop-mac';\n    }\n\n    _onFilesSelected(e) {\n        const $input = e.target;\n        const files = $input.files;\n\n        if (files.length === 0) return;\n\n        Events.fire('files-selected', {\n            files: files,\n            to: this._peer.id\n        });\n        $input.files = null; // reset input\n    }\n\n    setProgress(progress, status) {\n        const $progress = this.$el.querySelector('.progress');\n        if (0.5 < progress && progress < 1) {\n            $progress.classList.add('over50');\n        }\n        else {\n            $progress.classList.remove('over50');\n        }\n        if (progress < 1) {\n            if (status !== this.currentStatus) {\n                let statusName = {\n                    \"prepare\": Localization.getTranslation(\"peer-ui.preparing\"),\n                    \"transfer\": Localization.getTranslation(\"peer-ui.transferring\"),\n                    \"process\": Localization.getTranslation(\"peer-ui.processing\"),\n                    \"wait\": Localization.getTranslation(\"peer-ui.waiting\")\n                }[status];\n\n                this.$el.setAttribute('status', status);\n                this.$el.querySelector('.status').innerText = statusName;\n                this.currentStatus = status;\n            }\n        }\n        else {\n            this.$el.removeAttribute('status');\n            this.$el.querySelector('.status').innerHTML = '';\n            progress = 0;\n            this.currentStatus = null;\n        }\n        const degrees = `rotate(${360 * progress}deg)`;\n        $progress.style.setProperty('--progress', degrees);\n    }\n\n    _onDrop(e) {\n        if (this._shareMode.active || Dialog.anyDialogShown()) return;\n\n        e.preventDefault();\n\n        this._onDragEnd();\n\n        const peerId = this._peer.id;\n        const files = e.dataTransfer.files;\n        const text = e.dataTransfer.getData(\"text\");\n\n        if (files.length > 0) {\n            Events.fire('files-selected', {\n                files: files,\n                to: peerId\n            });\n        }\n        else if (text.length > 0) {\n            Events.fire('send-text', {\n                text: text,\n                to: peerId\n            });\n        }\n    }\n\n    _onDragOver() {\n        this.$el.setAttribute('drop', true);\n        this.$xInstructions.setAttribute('drop-peer', true);\n    }\n\n    _onDragEnd() {\n        this.$el.removeAttribute('drop');\n        this.$xInstructions.removeAttribute('drop-peer');\n    }\n\n    _onRightClick(e) {\n        e.preventDefault();\n        Events.fire('text-recipient', {\n            peerId: this._peer.id,\n            deviceName: e.target.closest('x-peer').querySelector('.name').innerText\n        });\n    }\n\n    _onTouchStart(e) {\n        this._touchStart = Date.now();\n        this._touchTimer = setTimeout(() => this._onTouchEnd(e), 610);\n    }\n\n    _onTouchEnd(e) {\n        if (Date.now() - this._touchStart < 500) {\n            clearTimeout(this._touchTimer);\n        }\n        else if (this._touchTimer) { // this was a long tap\n            e.preventDefault();\n            Events.fire('text-recipient', {\n                peerId: this._peer.id,\n                deviceName: e.target.closest('x-peer').querySelector('.name').innerText\n            });\n        }\n        this._touchTimer = null;\n    }\n}\n\nclass Dialog {\n    constructor(id) {\n        this.$el = $(id);\n        this.$autoFocus = this.$el.querySelector('[autofocus]');\n        this.$xBackground = this.$el.querySelector('x-background');\n        this.$closeBtns = this.$el.querySelectorAll('[close]');\n\n        this.$closeBtns.forEach(el => {\n            el.addEventListener('click', _ => this.hide())\n        });\n\n        Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));\n    }\n\n    static anyDialogShown() {\n        return document.querySelectorAll('x-dialog[show]').length > 0;\n    }\n\n    show() {\n        if (this.$xBackground) {\n            this.$xBackground.scrollTop = 0;\n        }\n\n        this.$el.setAttribute('show', true);\n\n        if (!window.isMobile && this.$autoFocus) {\n            this.$autoFocus.focus();\n        }\n    }\n\n    isShown() {\n        return !!this.$el.attributes[\"show\"];\n    }\n\n    hide() {\n        this.$el.removeAttribute('show');\n        if (!window.isMobile) {\n            document.activeElement.blur();\n            window.blur();\n        }\n        document.title = 'PairDrop | Transfer Files Cross-Platform. No Setup, No Signup.';\n        changeFavicon(\"images/favicon-96x96.png\");\n        this.correspondingPeerId = undefined;\n    }\n\n    _onPeerDisconnected(peerId) {\n        if (this.isShown() && this.correspondingPeerId === peerId) {\n            this.hide();\n            Events.fire('notify-user', Localization.getTranslation(\"notifications.selected-peer-left\"));\n        }\n    }\n\n    _evaluateOverflowing(element) {\n        if (element.clientHeight < element.scrollHeight) {\n            element.classList.add('overflowing');\n        }\n        else {\n            element.classList.remove('overflowing');\n        }\n    }\n}\n\nclass LanguageSelectDialog extends Dialog {\n\n    constructor() {\n        super('language-select-dialog');\n\n        this.$languageSelectBtn = $('language-selector');\n        this.$languageSelectBtn.addEventListener('click', _ => this.show());\n\n        this.$languageButtons = this.$el.querySelectorAll(\".language-buttons .btn\");\n        this.$languageButtons.forEach($btn => {\n            $btn.addEventListener(\"click\", e => this.selectLanguage(e));\n        })\n        Events.on('keydown', e => this._onKeyDown(e));\n    }\n\n    _onKeyDown(e) {\n        if (!this.isShown()) return;\n\n        if (e.code === \"Escape\") {\n            this.hide();\n        }\n    }\n\n    show() {\n        let locale = Localization.getLocale();\n        this.currentLanguageBtn = Localization.isSystemLocale()\n            ? this.$languageButtons[0]\n            : this.$el.querySelector(`.btn[value=\"${locale}\"]`);\n\n        this.currentLanguageBtn.classList.add(\"current\");\n\n        super.show();\n    }\n\n    hide() {\n        this.currentLanguageBtn.classList.remove(\"current\");\n\n        super.hide();\n    }\n\n    selectLanguage(e) {\n        e.preventDefault()\n        let languageCode = e.target.value;\n\n        if (languageCode) {\n            localStorage.setItem('language_code', languageCode);\n        }\n        else {\n            localStorage.removeItem('language_code');\n        }\n\n        Localization.setTranslation(languageCode)\n            .then(_ => this.hide());\n    }\n}\n\nclass ReceiveDialog extends Dialog {\n    constructor(id) {\n        super(id);\n        this.$fileDescription = this.$el.querySelector('.file-description');\n        this.$displayName = this.$el.querySelector('.display-name');\n        this.$fileStem = this.$el.querySelector('.file-stem');\n        this.$fileExtension = this.$el.querySelector('.file-extension');\n        this.$fileOther = this.$el.querySelector('.file-other');\n        this.$fileSize = this.$el.querySelector('.file-size');\n        this.$previewBox = this.$el.querySelector('.file-preview');\n        this.$receiveTitle = this.$el.querySelector('h2:first-of-type');\n    }\n\n    _formatFileSize(bytes) {\n        // 1 GB = 1024 MB = 1024^2 KB = 1024^3 B\n        // 1024^2 = 104876; 1024^3 = 1073741824\n        if (bytes >= 1073741824) {\n            return Math.round(10 * bytes / 1073741824) / 10 + ' GB';\n        }\n        else if (bytes >= 1048576) {\n            return Math.round(bytes / 1048576) + ' MB';\n        }\n        else if (bytes > 1024) {\n            return Math.round(bytes / 1024) + ' KB';\n        }\n        else {\n            return bytes + ' Bytes';\n        }\n    }\n\n    _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) {\n        let fileOther = \"\";\n\n        if (files.length === 2) {\n            fileOther = imagesOnly\n                ? Localization.getTranslation(\"dialogs.file-other-description-image\")\n                : Localization.getTranslation(\"dialogs.file-other-description-file\");\n        }\n        else if (files.length >= 2) {\n            fileOther = imagesOnly\n                ? Localization.getTranslation(\"dialogs.file-other-description-image-plural\", null, {count: files.length - 1})\n                : Localization.getTranslation(\"dialogs.file-other-description-file-plural\", null, {count: files.length - 1});\n        }\n\n        this.$fileOther.innerText = fileOther;\n\n        const fileName = files[0].name;\n        const fileNameSplit = fileName.split('.');\n        const fileExtension = fileNameSplit.length > 1\n            ? '.' + fileNameSplit[fileNameSplit.length - 1]\n            : '';\n        this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);\n        this.$fileExtension.innerText = fileExtension;\n        this.$fileSize.innerText = this._formatFileSize(totalSize);\n        this.$displayName.innerText = displayName;\n        this.$displayName.title = connectionHash;\n        this.$displayName.classList.remove(\"badge-room-ip\", \"badge-room-secret\", \"badge-room-public-id\");\n        this.$displayName.classList.add(badgeClassName)\n    }\n}\n\nclass ReceiveFileDialog extends ReceiveDialog {\n\n    constructor() {\n        super('receive-file-dialog');\n\n        this.$downloadBtn = this.$el.querySelector('#download-btn');\n        this.$shareBtn = this.$el.querySelector('#share-btn');\n\n        Events.on('files-received', e => this._onFilesReceived(e.detail.peerId, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));\n        this._filesQueue = [];\n    }\n\n    async _onFilesReceived(peerId, files, imagesOnly, totalSize) {\n        const displayName = $(peerId).ui._displayName();\n        const connectionHash = $(peerId).ui._connectionHash;\n        const badgeClassName = $(peerId).ui._badgeClassName();\n\n        this._filesQueue.push({\n            peerId: peerId,\n            displayName: displayName,\n            connectionHash: connectionHash,\n            files: files,\n            imagesOnly: imagesOnly,\n            totalSize: totalSize,\n            badgeClassName: badgeClassName\n        });\n\n        window.blop.play();\n\n        await this._nextFiles();\n    }\n\n    async _nextFiles() {\n        if (this._busy || !this._filesQueue.length) return;\n        this._busy = true;\n        const {peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName} = this._filesQueue.shift();\n        await this._displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName);\n    }\n\n    createPreviewElement(file) {\n        return new Promise((resolve, reject) => {\n            try {\n                let mime = file.type.split('/')[0]\n                let previewElement = {\n                    image: 'img',\n                    audio: 'audio',\n                    video: 'video'\n                }\n\n                if (Object.keys(previewElement).indexOf(mime) === -1) {\n                    resolve(false);\n                }\n                else {\n                    let element = document.createElement(previewElement[mime]);\n                    element.controls = true;\n                    element.onload = _ => {\n                        this.$previewBox.appendChild(element);\n                        resolve(true);\n                    };\n                    element.onloadeddata = _ => {\n                        this.$previewBox.appendChild(element);\n                        resolve(true);\n                    };\n                    element.onerror = _ => {\n                        reject(`${mime} preview could not be loaded from type ${file.type}`);\n                    };\n                    element.src = URL.createObjectURL(file);\n                }\n            } catch (e) {\n                reject(`preview could not be loaded from type ${file.type}`);\n            }\n        });\n    }\n\n    async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) {\n        this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName);\n\n        let descriptor, url, filenameDownload;\n        if (files.length === 1) {\n            descriptor = imagesOnly\n                ? Localization.getTranslation(\"dialogs.title-image\")\n                : Localization.getTranslation(\"dialogs.title-file\");\n        }\n        else {\n            descriptor = imagesOnly\n                ? Localization.getTranslation(\"dialogs.title-image-plural\")\n                : Localization.getTranslation(\"dialogs.title-file-plural\");\n        }\n        this.$receiveTitle.innerText = Localization.getTranslation(\"dialogs.receive-title\", null, {descriptor: descriptor});\n\n        const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});\n        if (canShare) {\n            this.$shareBtn.removeAttribute('hidden');\n            this.$shareBtn.onclick = _ => {\n                navigator.share({files: files})\n                    .catch(err => {\n                        console.error(err);\n                    });\n            }\n        }\n\n        let downloadZipped = false;\n        if (files.length > 1) {\n            downloadZipped = true;\n            try {\n                let bytesCompleted = 0;\n                zipper.createNewZipWriter();\n                for (let i=0; i<files.length; i++) {\n                    await zipper.addFile(files[i], {\n                        onprogress: (progress) => {\n                            Events.fire('set-progress', {\n                                peerId: peerId,\n                                progress: (bytesCompleted + progress) / totalSize,\n                                status: 'process'\n                            })\n                        }\n                    });\n                    bytesCompleted += files[i].size;\n                }\n                url = await zipper.getBlobURL();\n\n                let now = new Date(Date.now());\n                let year = now.getFullYear().toString();\n                let month = (now.getMonth()+1).toString();\n                month = month.length < 2 ? \"0\" + month : month;\n                let date = now.getDate().toString();\n                date = date.length < 2 ? \"0\" + date : date;\n                let hours = now.getHours().toString();\n                hours = hours.length < 2 ? \"0\" + hours : hours;\n                let minutes = now.getMinutes().toString();\n                minutes = minutes.length < 2 ? \"0\" + minutes : minutes;\n                filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;\n            } catch (e) {\n                console.error(e);\n                downloadZipped = false;\n            }\n        }\n\n        this.$downloadBtn.removeAttribute('disabled');\n        this.$downloadBtn.innerText = Localization.getTranslation(\"dialogs.download\");\n        this.$downloadBtn.onclick = _ => {\n            if (downloadZipped) {\n                let tmpZipBtn = document.createElement(\"a\");\n                tmpZipBtn.download = filenameDownload;\n                tmpZipBtn.href = url;\n                tmpZipBtn.click();\n            }\n            else {\n                this._downloadFilesIndividually(files);\n            }\n\n            if (!canShare) {\n                this.$downloadBtn.innerText = Localization.getTranslation(\"dialogs.download-again\");\n            }\n            Events.fire('notify-user', Localization.getTranslation(\"notifications.download-successful\", null, {descriptor: descriptor}));\n\n            // Prevent clicking the button multiple times\n            this.$downloadBtn.style.pointerEvents = \"none\";\n            setTimeout(() => this.$downloadBtn.style.pointerEvents = \"unset\", 2000);\n        };\n\n        document.title = files.length === 1\n            ? `${ Localization.getTranslation(\"document-titles.file-received\") } - PairDrop`\n            : `${ Localization.getTranslation(\"document-titles.file-received-plural\", null, {count: files.length}) } - PairDrop`;\n        changeFavicon(\"images/favicon-96x96-notification.png\");\n\n        Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})\n        this.show();\n\n        setTimeout(() => {\n            // wait for the dialog to be shown\n            if (canShare) {\n                this.$shareBtn.click();\n            }\n            else {\n                this.$downloadBtn.click();\n            }\n        }, 500);\n\n        this.createPreviewElement(files[0])\n            .then(canPreview => {\n                if (canPreview) {\n                    console.log('the file is able to preview');\n                }\n                else {\n                    console.log('the file is not able to preview');\n                }\n            })\n            .catch(r => console.error(r));\n    }\n\n    _downloadFilesIndividually(files) {\n        let tmpBtn = document.createElement(\"a\");\n        for (let i=0; i<files.length; i++) {\n            tmpBtn.download = files[i].name;\n            tmpBtn.href = URL.createObjectURL(files[i]);\n            tmpBtn.click();\n        }\n    }\n\n    hide() {\n        super.hide();\n        setTimeout(async () => {\n            this.$shareBtn.setAttribute('hidden', true);\n            this.$downloadBtn.setAttribute('disabled', true);\n            this.$previewBox.innerHTML = '';\n            this._busy = false;\n            await this._nextFiles();\n        }, 300);\n    }\n}\n\nclass ReceiveRequestDialog extends ReceiveDialog {\n\n    constructor() {\n        super('receive-request-dialog');\n\n        this.$acceptRequestBtn = this.$el.querySelector('#accept-request');\n        this.$declineRequestBtn = this.$el.querySelector('#decline-request');\n        this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));\n        this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));\n\n        Events.on('files-transfer-request', e => this._onRequestFileTransfer(e.detail.request, e.detail.peerId))\n        Events.on('keydown', e => this._onKeyDown(e));\n        this._filesTransferRequestQueue = [];\n    }\n\n    _onKeyDown(e) {\n        if (!this.isShown()) return;\n\n        if (e.code === \"Escape\") {\n            this._respondToFileTransferRequest(false);\n        }\n    }\n\n    _onRequestFileTransfer(request, peerId) {\n        this._filesTransferRequestQueue.push({request: request, peerId: peerId});\n        if (this.isShown()) return;\n        this._dequeueRequests();\n    }\n\n    _dequeueRequests() {\n        if (!this._filesTransferRequestQueue.length) return;\n        let {request, peerId} = this._filesTransferRequestQueue.shift();\n        this._showRequestDialog(request, peerId)\n    }\n\n    _showRequestDialog(request, peerId) {\n        this.correspondingPeerId = peerId;\n\n        const displayName = $(peerId).ui._displayName();\n        const connectionHash = $(peerId).ui._connectionHash;\n\n        const badgeClassName = $(peerId).ui._badgeClassName();\n\n        this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize, badgeClassName);\n\n        if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === \"data:image/jpeg;base64\") {\n            let element = document.createElement('img');\n            element.src = request.thumbnailDataUrl;\n            this.$previewBox.appendChild(element)\n        }\n\n        const transferRequestTitle= request.imagesOnly\n            ? Localization.getTranslation('document-titles.image-transfer-requested')\n            : Localization.getTranslation('document-titles.file-transfer-requested');\n\n        this.$receiveTitle.innerText = transferRequestTitle;\n\n        document.title =  `${transferRequestTitle} - PairDrop`;\n        changeFavicon(\"images/favicon-96x96-notification.png\");\n\n        this.$acceptRequestBtn.removeAttribute('disabled');\n        this.show();\n    }\n\n    _respondToFileTransferRequest(accepted) {\n        Events.fire('respond-to-files-transfer-request', {\n            to: this.correspondingPeerId,\n            accepted: accepted\n        })\n        if (accepted) {\n            Events.fire('set-progress', {peerId: this.correspondingPeerId, progress: 0, status: 'wait'});\n            NoSleepUI.enable();\n        }\n        this.hide();\n    }\n\n    hide() {\n        // clear previewBox after dialog is closed\n        setTimeout(() => {\n            this.$previewBox.innerHTML = '';\n            this.$acceptRequestBtn.setAttribute('disabled', true);\n        }, 300);\n\n        super.hide();\n\n        // show next request\n        setTimeout(() => this._dequeueRequests(), 300);\n    }\n}\n\nclass InputKeyContainer {\n    constructor(inputKeyContainer, evaluationRegex, onAllCharsFilled, onNoAllCharsFilled, onLastCharFilled) {\n\n        this.$inputKeyContainer = inputKeyContainer;\n        this.$inputKeyChars = inputKeyContainer.querySelectorAll('input');\n\n        this.$inputKeyChars.forEach(char => char.addEventListener('input', e => this._onCharsInput(e)));\n        this.$inputKeyChars.forEach(char => char.addEventListener('keydown', e => this._onCharsKeyDown(e)));\n        this.$inputKeyChars.forEach(char => char.addEventListener('keyup', e => this._onCharsKeyUp(e)));\n        this.$inputKeyChars.forEach(char => char.addEventListener('focus', e => e.target.select()));\n        this.$inputKeyChars.forEach(char => char.addEventListener('click', e => e.target.select()));\n\n        this.evalRgx = evaluationRegex\n\n        this._onAllCharsFilled = onAllCharsFilled;\n        this._onNotAllCharsFilled = onNoAllCharsFilled;\n        this._onLastCharFilled = onLastCharFilled;\n    }\n\n    _enableChars() {\n        this.$inputKeyChars.forEach(char => char.removeAttribute('disabled'));\n    }\n\n    _disableChars() {\n        this.$inputKeyChars.forEach(char => char.setAttribute('disabled', true));\n    }\n\n    _clearChars() {\n        this.$inputKeyChars.forEach(char => char.value = '');\n    }\n\n    _cleanUp() {\n        this._clearChars();\n        this._disableChars();\n    }\n\n    _onCharsInput(e) {\n        if (!e.target.value.match(this.evalRgx)) {\n            e.target.value = '';\n            return;\n        }\n        this._evaluateKeyChars();\n\n        let nextSibling = e.target.nextElementSibling;\n        if (nextSibling) {\n            e.preventDefault();\n            nextSibling.focus();\n        }\n    }\n\n    _onCharsKeyDown(e) {\n        let previousSibling = e.target.previousElementSibling;\n        let nextSibling = e.target.nextElementSibling;\n        if (e.key === \"Backspace\" && previousSibling && !e.target.value) {\n            previousSibling.value = '';\n            previousSibling.focus();\n        }\n        else if (e.key === \"ArrowRight\" && nextSibling) {\n            e.preventDefault();\n            nextSibling.focus();\n        }\n        else if (e.key === \"ArrowLeft\" && previousSibling) {\n            e.preventDefault();\n            previousSibling.focus();\n        }\n    }\n\n    _onCharsKeyUp(e) {\n        // deactivate submit btn when e.g. using backspace to clear element\n        if (!e.target.value) {\n            this._evaluateKeyChars();\n        }\n    }\n\n    _getInputKey() {\n        let key = \"\";\n        this.$inputKeyChars.forEach(char => {\n            key += char.value;\n        })\n        return key;\n    }\n\n    _onPaste(pastedKey) {\n        let rgx = new RegExp(\"(?!\" + this.evalRgx.source + \").\", \"g\");\n        pastedKey = pastedKey.replace(rgx,'').substring(0, this.$inputKeyChars.length)\n        for (let i = 0; i < pastedKey.length; i++) {\n            document.activeElement.value = pastedKey.charAt(i);\n            let nextSibling = document.activeElement.nextElementSibling;\n            if (!nextSibling) break;\n            nextSibling.focus();\n        }\n        this._evaluateKeyChars();\n    }\n\n    _evaluateKeyChars() {\n        if (this.$inputKeyContainer.querySelectorAll('input:placeholder-shown').length > 0) {\n            this._onNotAllCharsFilled();\n        }\n        else {\n            this._onAllCharsFilled();\n\n            const lastCharFocused = document.activeElement === this.$inputKeyChars[this.$inputKeyChars.length - 1];\n            if (lastCharFocused) {\n                this._onLastCharFilled();\n            }\n        }\n    }\n\n    focusLastChar() {\n        let lastChar = this.$inputKeyChars[this.$inputKeyChars.length-1];\n        lastChar.focus();\n    }\n}\n\nclass PairDeviceDialog extends Dialog {\n    constructor() {\n        super('pair-device-dialog');\n        this.$pairDeviceHeaderBtn = $('pair-device');\n        this.$editPairedDevicesHeaderBtn = $('edit-paired-devices');\n        this.$footerInstructionsPairedDevices = $$('.discovery-wrapper .badge-room-secret');\n\n        this.$key = this.$el.querySelector('.key');\n        this.$qrCode = this.$el.querySelector('.key-qr-code');\n        this.$form = this.$el.querySelector('form');\n        this.$closeBtn = this.$el.querySelector('[close]')\n        this.$pairSubmitBtn = this.$el.querySelector('button[type=\"submit\"]');\n\n        this.inputKeyContainer = new InputKeyContainer(\n            this.$el.querySelector('.input-key-container'),\n            /\\d/,\n            () => this.$pairSubmitBtn.removeAttribute('disabled'),\n            () => this.$pairSubmitBtn.setAttribute('disabled', true),\n            () => this._submit()\n        );\n\n        this.$pairDeviceHeaderBtn.addEventListener('click', _ => this._pairDeviceInitiate());\n        this.$form.addEventListener('submit', e => this._onSubmit(e));\n        this.$closeBtn.addEventListener('click', _ => this._close());\n\n        Events.on('keydown', e => this._onKeyDown(e));\n        Events.on('ws-disconnected', _ => this.hide());\n        Events.on('pair-device-initiated', e => this._onPairDeviceInitiated(e.detail));\n        Events.on('pair-device-joined', e => this._onPairDeviceJoined(e.detail.peerId, e.detail.roomSecret));\n        Events.on('peers', e => this._onPeers(e.detail));\n        Events.on('peer-joined', e => this._onPeerJoined(e.detail));\n        Events.on('pair-device-join-key-invalid', _ => this._onPublicRoomJoinKeyInvalid());\n        Events.on('pair-device-canceled', e => this._onPairDeviceCanceled(e.detail));\n        Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets())\n        Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));\n        this.$el.addEventListener('paste', e => this._onPaste(e));\n        this.$qrCode.addEventListener('click', _ => this._copyPairUrl());\n\n        this.pairPeer = {};\n    }\n\n    _onKeyDown(e) {\n        if (!this.isShown()) return;\n\n        if (e.code === \"Escape\") {\n            // Timeout to prevent share mode from getting cancelled simultaneously\n            setTimeout(() => this._close(), 50);\n        }\n    }\n\n    _onPaste(e) {\n        e.preventDefault();\n        let pastedKey = e.clipboardData\n            .getData(\"Text\")\n            .replace(/\\D/g,'')\n            .substring(0, 6);\n        this.inputKeyContainer._onPaste(pastedKey);\n    }\n\n    _pairDeviceInitiate() {\n        Events.fire('pair-device-initiate');\n    }\n\n    _onPairDeviceInitiated(msg) {\n        this.pairKey = msg.pairKey;\n        this.roomSecret = msg.roomSecret;\n        this._setKeyAndQRCode();\n        this.inputKeyContainer._enableChars();\n        this.show();\n    }\n\n    _setKeyAndQRCode() {\n        this.$key.innerText = `${this.pairKey.substring(0,3)} ${this.pairKey.substring(3,6)}`\n\n        // Display the QR code for the url\n        const qr = new QRCode({\n            content: this._getPairUrl(),\n            width: 130,\n            height: 130,\n            padding: 1,\n            background: 'white',\n            color: 'rgb(18, 18, 18)',\n            ecl: \"L\",\n            join: true\n        });\n        this.$qrCode.innerHTML = qr.svg();\n    }\n\n    _getPairUrl() {\n        let url = new URL(location.href);\n        url.searchParams.append('pair_key', this.pairKey)\n        return url.href;\n    }\n\n    _copyPairUrl() {\n        navigator.clipboard.writeText(this._getPairUrl())\n            .then(_ => {\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.pair-url-copied-to-clipboard\"));\n            })\n            .catch(_ => {\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.copied-to-clipboard-error\"));\n            })\n    }\n\n    _onSubmit(e) {\n        e.preventDefault();\n        this._submit();\n    }\n\n    _submit() {\n        let inputKey = this.inputKeyContainer._getInputKey();\n        this._pairDeviceJoin(inputKey);\n    }\n\n    _pairDeviceJoin(pairKey) {\n        if (/^\\d{6}$/g.test(pairKey)) {\n            Events.fire('pair-device-join', pairKey);\n            this.inputKeyContainer.focusLastChar();\n        }\n    }\n\n    _onPairDeviceJoined(peerId, roomSecret) {\n        // abort if peer is another tab on the same browser and remove room-type from gui\n        if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {\n            this._cleanUp();\n            this.hide();\n\n            Events.fire('room-secrets-deleted', [roomSecret]);\n\n            Events.fire('notify-user', Localization.getTranslation(\"notifications.pairing-tabs-error\"));\n            return;\n        }\n\n        // save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret\n        this.pairPeer = {\n            \"peerId\": peerId,\n            \"roomSecret\": roomSecret\n        };\n    }\n\n    _onPeers(message) {\n        message.peers.forEach(messagePeer => {\n            this._evaluateJoinedPeer(messagePeer.id, message.roomType, message.roomId);\n        });\n    }\n\n    _onPeerJoined(message) {\n        this._evaluateJoinedPeer(message.peer.id, message.roomType, message.roomId);\n    }\n\n    _evaluateJoinedPeer(peerId, roomType, roomId) {\n        const noPairPeerSaved = !Object.keys(this.pairPeer);\n\n        if (!peerId || !roomType || !roomId || noPairPeerSaved) return;\n\n        const samePeerId = peerId === this.pairPeer.peerId;\n        const sameRoomSecret = roomId === this.pairPeer.roomSecret;\n        const typeIsSecret = roomType === \"secret\";\n\n        if (!samePeerId || !sameRoomSecret || !typeIsSecret) return;\n\n        this._onPairPeerJoined(peerId, roomId);\n        this.pairPeer = {};\n    }\n\n    _onPairPeerJoined(peerId, roomSecret) {\n        // if devices are paired that are already connected we must save the names at this point\n        const $peer = $(peerId);\n        let displayName, deviceName;\n        if ($peer) {\n            displayName = $peer.ui._peer.name.displayName;\n            deviceName = $peer.ui._peer.name.deviceName;\n        }\n\n        PersistentStorage\n            .addRoomSecret(roomSecret, displayName, deviceName)\n            .then(_ => {\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.pairing-success\"));\n                this._evaluateNumberRoomSecrets();\n            })\n            .finally(() => {\n                this._cleanUp();\n                this.hide();\n            })\n            .catch(_ => {\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.pairing-not-persistent\"));\n                PersistentStorage.logBrowserNotCapable();\n            });\n    }\n\n    _onPublicRoomJoinKeyInvalid() {\n        Events.fire('notify-user', Localization.getTranslation(\"notifications.pairing-key-invalid\"));\n    }\n\n    _close() {\n        this._pairDeviceCancel();\n    }\n\n    _pairDeviceCancel() {\n        this.hide();\n        this._cleanUp();\n        Events.fire('pair-device-cancel');\n    }\n\n    _onPairDeviceCanceled(pairKey) {\n        Events.fire('notify-user', Localization.getTranslation(\"notifications.pairing-key-invalidated\", null, {key: pairKey}));\n    }\n\n    _cleanUp() {\n        this.roomSecret = null;\n        this.pairKey = null;\n        this.inputKeyContainer._cleanUp();\n        this.pairPeer = {};\n    }\n\n    _onSecretRoomDeleted(roomSecret) {\n        PersistentStorage\n            .deleteRoomSecret(roomSecret)\n            .then(_ => {\n                this._evaluateNumberRoomSecrets();\n            });\n    }\n\n    _evaluateNumberRoomSecrets() {\n        PersistentStorage\n            .getAllRoomSecrets()\n            .then(roomSecrets => {\n                if (roomSecrets.length > 0) {\n                    this.$editPairedDevicesHeaderBtn.removeAttribute('hidden');\n                    this.$footerInstructionsPairedDevices.removeAttribute('hidden');\n                }\n                else {\n                    this.$editPairedDevicesHeaderBtn.setAttribute('hidden', true);\n                    this.$footerInstructionsPairedDevices.setAttribute('hidden', true);\n                }\n                Events.fire('evaluate-footer-badges');\n            });\n    }\n}\n\nclass EditPairedDevicesDialog extends Dialog {\n    constructor() {\n        super('edit-paired-devices-dialog');\n        this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper');\n        this.$footerBadgePairedDevices = $$('.discovery-wrapper .badge-room-secret');\n\n        $('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices());\n        this.$footerBadgePairedDevices.addEventListener('click', _ => this._onEditPairedDevices());\n\n        Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));\n        Events.on('keydown', e => this._onKeyDown(e));\n    }\n\n    _onKeyDown(e) {\n        if (!this.isShown()) return;\n\n        if (e.code === \"Escape\") {\n            this.hide();\n        }\n    }\n\n    async _initDOM() {\n        const pairedDeviceRemovedString = Localization.getTranslation(\"dialogs.paired-device-removed\");\n        const unpairString = Localization.getTranslation(\"dialogs.unpair\").toUpperCase();\n        const autoAcceptString = Localization.getTranslation(\"dialogs.auto-accept\").toLowerCase();\n        const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries();\n\n        roomSecretsEntries\n            .forEach(roomSecretsEntry => {\n                let $pairedDevice = document.createElement('div');\n                $pairedDevice.classList.add(\"paired-device\");\n                $pairedDevice.setAttribute('placeholder', pairedDeviceRemovedString);\n\n                $pairedDevice.innerHTML = `\n                    <div class=\"display-name\">\n                        <span class=\"fw\">\n                            ${roomSecretsEntry.display_name}\n                        </span>\n                    </div>\n                    <div class=\"device-name\">\n                        <span class=\"fw\">\n                            ${roomSecretsEntry.device_name}\n                        </span>\n                    </div>\n                    <div class=\"button-wrapper row fw center wrap\">\n                        <div class=\"center grow\">\n                            <span class=\"center wrap\">\n                                ${autoAcceptString}\n                            </span>\n                            <label class=\"auto-accept switch pointer m-1\">\n                                <input type=\"checkbox\" ${roomSecretsEntry.auto_accept ? \"checked\" : \"\"}>\n                                <div class=\"slider round\"></div>\n                            </label>\n                        </div>\n                        <button class=\"btn grow\" type=\"button\">${unpairString}</button>\n                    </div>`\n\n                $pairedDevice\n                    .querySelector('input[type=\"checkbox\"]')\n                    .addEventListener('click', e => {\n                        PersistentStorage\n                            .updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked)\n                            .then(roomSecretsEntry => {\n                                Events.fire('auto-accept-updated', {\n                                    'roomSecret': roomSecretsEntry.entry.secret,\n                                    'autoAccept': e.target.checked\n                                });\n                            });\n                    });\n\n                $pairedDevice\n                    .querySelector('button')\n                    .addEventListener('click', e => {\n                        PersistentStorage\n                            .deleteRoomSecret(roomSecretsEntry.secret)\n                            .then(roomSecret => {\n                                Events.fire('room-secrets-deleted', [roomSecret]);\n                                Events.fire('evaluate-number-room-secrets');\n                                $pairedDevice.innerText = \"\";\n                            });\n                    })\n\n                this.$pairedDevicesWrapper.appendChild($pairedDevice)\n            })\n    }\n\n    hide() {\n        super.hide();\n        setTimeout(() => {\n            this.$pairedDevicesWrapper.innerHTML = \"\"\n        }, 300);\n    }\n\n    _onEditPairedDevices() {\n        this._initDOM()\n            .then(_ => {\n                this._evaluateOverflowing(this.$pairedDevicesWrapper);\n                this.show();\n            });\n    }\n\n    _clearRoomSecrets() {\n        PersistentStorage\n            .getAllRoomSecrets()\n            .then(roomSecrets => {\n                PersistentStorage\n                    .clearRoomSecrets()\n                    .finally(() => {\n                        Events.fire('room-secrets-deleted', roomSecrets);\n                        Events.fire('evaluate-number-room-secrets');\n                        Events.fire('notify-user', Localization.getTranslation(\"notifications.pairing-cleared\"));\n                        this.hide();\n                    })\n            });\n    }\n\n    _onPeerDisplayNameChanged(e) {\n        const peerId = e.detail.peerId;\n        const peerNode = $(peerId);\n\n        if (!peerNode) return;\n\n        const peer = peerNode.ui._peer;\n\n        if (!peer || !peer._roomIds[\"secret\"]) return;\n\n        PersistentStorage\n            .updateRoomSecretNames(peer._roomIds[\"secret\"], peer.name.displayName, peer.name.deviceName)\n            .then(roomSecretEntry => {\n                console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`);\n            })\n    }\n}\n\nclass PublicRoomDialog extends Dialog {\n    constructor() {\n        super('public-room-dialog');\n\n        this.$key = this.$el.querySelector('.key');\n        this.$qrCode = this.$el.querySelector('.key-qr-code');\n        this.$form = this.$el.querySelector('form');\n        this.$closeBtn = this.$el.querySelector('[close]');\n        this.$leaveBtn = this.$el.querySelector('.leave-room');\n        this.$joinSubmitBtn = this.$el.querySelector('button[type=\"submit\"]');\n        this.$headerBtnJoinPublicRoom = $('join-public-room');\n        this.$footerBadgePublicRoomDevices = $$('.discovery-wrapper .badge-room-public-id');\n\n\n        this.$form.addEventListener('submit', e => this._onSubmit(e));\n        this.$closeBtn.addEventListener('click', _ => this.hide());\n        this.$leaveBtn.addEventListener('click', _ => this._leavePublicRoom())\n\n        this.$headerBtnJoinPublicRoom.addEventListener('click', _ => this._onHeaderBtnClick());\n        this.$footerBadgePublicRoomDevices.addEventListener('click', _ => this._onHeaderBtnClick());\n\n        this.inputKeyContainer = new InputKeyContainer(\n            this.$el.querySelector('.input-key-container'),\n            /[a-z|A-Z]/,\n            () => this.$joinSubmitBtn.removeAttribute('disabled'),\n            () => this.$joinSubmitBtn.setAttribute('disabled', true),\n            () => this._submit()\n        );\n\n        Events.on('keydown', e => this._onKeyDown(e));\n        Events.on('public-room-created', e => this._onPublicRoomCreated(e.detail));\n        Events.on('peers', e => this._onPeers(e.detail));\n        Events.on('peer-joined', e => this._onPeerJoined(e.detail));\n        Events.on('public-room-id-invalid', e => this._onPublicRoomIdInvalid(e.detail));\n        Events.on('public-room-left', _ => this._onPublicRoomLeft());\n        this.$el.addEventListener('paste', e => this._onPaste(e));\n        this.$qrCode.addEventListener('click', _ => this._copyShareRoomUrl());\n\n        Events.on('ws-connected', _ => this._onWsConnected());\n        Events.on('translation-loaded', _ => this.setFooterBadge());\n    }\n\n    _onKeyDown(e) {\n        if (!this.isShown()) return;\n\n        if (e.code === \"Escape\") {\n            this.hide();\n        }\n    }\n\n    _onPaste(e) {\n        e.preventDefault();\n        let pastedKey = e.clipboardData.getData(\"Text\");\n        this.inputKeyContainer._onPaste(pastedKey);\n    }\n\n    _onHeaderBtnClick() {\n        if (this.roomId) {\n            this.show();\n        }\n        else {\n            this._createPublicRoom();\n        }\n    }\n\n    _createPublicRoom() {\n        Events.fire('create-public-room');\n    }\n\n    _onPublicRoomCreated(roomId) {\n        this.roomId = roomId;\n\n        this._setKeyAndQrCode();\n\n        this.show();\n\n        sessionStorage.setItem('public_room_id', roomId);\n    }\n\n    _setKeyAndQrCode() {\n        if (!this.roomId) return;\n\n        this.$key.innerText = this.roomId.toUpperCase();\n\n        // Display the QR code for the url\n        const qr = new QRCode({\n            content: this._getShareRoomUrl(),\n            width: 130,\n            height: 130,\n            padding: 1,\n            background: 'white',\n            color: 'rgb(18, 18, 18)',\n            ecl: \"L\",\n            join: true\n        });\n        this.$qrCode.innerHTML = qr.svg();\n\n        this.setFooterBadge();\n    }\n\n    setFooterBadge() {\n        if (!this.roomId) return;\n\n        this.$footerBadgePublicRoomDevices.innerText = Localization.getTranslation(\"footer.public-room-devices\", null, {\n            roomId: this.roomId.toUpperCase()\n        });\n        this.$footerBadgePublicRoomDevices.removeAttribute('hidden');\n\n        Events.fire('evaluate-footer-badges');\n    }\n\n    _getShareRoomUrl() {\n        let url = new URL(location.href);\n        url.searchParams.append('room_id', this.roomId)\n        return url.href;\n    }\n\n    _copyShareRoomUrl() {\n        navigator.clipboard.writeText(this._getShareRoomUrl())\n            .then(_ => {\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.room-url-copied-to-clipboard\"));\n            })\n            .catch(_ => {\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.copied-to-clipboard-error\"));\n            })\n    }\n\n    _onWsConnected() {\n        let roomId = sessionStorage.getItem('public_room_id');\n\n        if (!roomId) return;\n\n        this.roomId = roomId;\n        this._setKeyAndQrCode();\n\n        this._joinPublicRoom(roomId, true);\n    }\n\n    _onSubmit(e) {\n        e.preventDefault();\n        this._submit();\n    }\n\n    _submit() {\n        let inputKey = this.inputKeyContainer._getInputKey();\n        this._joinPublicRoom(inputKey);\n    }\n\n    _joinPublicRoom(roomId, createIfInvalid = false) {\n        roomId = roomId.toLowerCase();\n        if (/^[a-z]{5}$/g.test(roomId)) {\n            this.roomIdJoin = roomId;\n\n            this.inputKeyContainer.focusLastChar();\n\n            Events.fire('join-public-room', {\n                roomId: roomId,\n                createIfInvalid: createIfInvalid\n            });\n        }\n    }\n\n    _onPeers(message) {\n        message.peers.forEach(messagePeer => {\n            this._evaluateJoinedPeer(messagePeer.id, message.roomId);\n        });\n    }\n\n    _onPeerJoined(message) {\n        this._evaluateJoinedPeer(message.peer.id, message.roomId);\n    }\n\n    _evaluateJoinedPeer(peerId, roomId) {\n        const isInitiatedRoomId = roomId === this.roomId;\n        const isJoinedRoomId = roomId === this.roomIdJoin;\n\n        if (!peerId || !roomId || !(isInitiatedRoomId || isJoinedRoomId)) return;\n\n        this.hide();\n\n        sessionStorage.setItem('public_room_id', roomId);\n\n        if (isJoinedRoomId) {\n            this.roomId = roomId;\n            this.roomIdJoin = false;\n            this._setKeyAndQrCode();\n        }\n    }\n\n    _onPublicRoomIdInvalid(roomId) {\n        Events.fire('notify-user', Localization.getTranslation(\"notifications.public-room-id-invalid\"));\n        if (roomId === sessionStorage.getItem('public_room_id')) {\n            sessionStorage.removeItem('public_room_id');\n        }\n    }\n\n    _leavePublicRoom() {\n        Events.fire('leave-public-room', this.roomId);\n    }\n\n    _onPublicRoomLeft() {\n        let publicRoomId = this.roomId.toUpperCase();\n        this.hide();\n        this._cleanUp();\n        Events.fire('notify-user', Localization.getTranslation(\"notifications.public-room-left\", null, {publicRoomId: publicRoomId}));\n    }\n\n    show() {\n        this.inputKeyContainer._enableChars();\n        super.show();\n    }\n\n    hide() {\n        this.inputKeyContainer._cleanUp();\n        super.hide();\n    }\n\n    _cleanUp() {\n        this.roomId = null;\n        this.inputKeyContainer._cleanUp();\n        sessionStorage.removeItem('public_room_id');\n        this.$footerBadgePublicRoomDevices.setAttribute('hidden', true);\n        Events.fire('evaluate-footer-badges');\n    }\n}\n\nclass SendTextDialog extends Dialog {\n    constructor() {\n        super('send-text-dialog');\n\n        this.$text = this.$el.querySelector('.textarea');\n        this.$peerDisplayName = this.$el.querySelector('.display-name');\n        this.$form = this.$el.querySelector('form');\n        this.$submit = this.$el.querySelector('button[type=\"submit\"]');\n        this.$form.addEventListener('submit', e => this._onSubmit(e));\n        this.$text.addEventListener('input', _ => this._onInput());\n        this.$text.addEventListener('paste', e => this._onPaste(e));\n        this.$text.addEventListener('drop', e => this._onDrop(e));\n\n        Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));\n        Events.on('keydown', e => this._onKeyDown(e));\n    }\n\n    _onKeyDown(e) {\n        if (!this.isShown()) return;\n\n        if (e.code === \"Escape\") {\n            this.hide();\n        }\n        else if (e.code === \"Enter\" && (e.ctrlKey || e.metaKey)) {\n            if (this._textEmpty()) return;\n\n            this._send();\n        }\n    }\n\n    async _onDrop(e) {\n        e.preventDefault()\n\n        const text = e.dataTransfer.getData(\"text\");\n        const selection = window.getSelection();\n\n        if (selection.rangeCount) {\n            selection.deleteFromDocument();\n            selection.getRangeAt(0).insertNode(document.createTextNode(text));\n        }\n\n        this._onInput();\n    }\n\n    async _onPaste(e) {\n        e.preventDefault()\n\n        const text = (e.clipboardData || window.clipboardData).getData('text');\n        const selection = window.getSelection();\n\n        if (selection.rangeCount) {\n            selection.deleteFromDocument();\n            const textNode = document.createTextNode(text);\n            const range = document.createRange();\n            range.setStart(textNode, textNode.length);\n            range.collapse(true);\n            selection.getRangeAt(0).insertNode(textNode);\n            selection.removeAllRanges();\n            selection.addRange(range);\n        }\n\n        this._onInput();\n    }\n\n    _textEmpty() {\n        return !this.$text.innerText || this.$text.innerText === \"\\n\";\n    }\n\n    _onInput() {\n        if (this._textEmpty()) {\n            this.$submit.setAttribute('disabled', true);\n            // remove remaining whitespace on Firefox on text deletion\n            this.$text.innerText = \"\";\n        }\n        else {\n            this.$submit.removeAttribute('disabled');\n        }\n        this._evaluateOverflowing(this.$text);\n    }\n\n    _onRecipient(peerId, deviceName) {\n        this.correspondingPeerId = peerId;\n        this.$peerDisplayName.innerText = deviceName;\n        this.$peerDisplayName.classList.remove(\"badge-room-ip\", \"badge-room-secret\", \"badge-room-public-id\");\n        this.$peerDisplayName.classList.add($(peerId).ui._badgeClassName());\n\n        this.show();\n\n        const range = document.createRange();\n        const sel = window.getSelection();\n\n        range.selectNodeContents(this.$text);\n        sel.removeAllRanges();\n        sel.addRange(range);\n    }\n\n    _onSubmit(e) {\n        e.preventDefault();\n        this._send();\n    }\n\n    _send() {\n        Events.fire('send-text', {\n            to: this.correspondingPeerId,\n            text: this.$text.innerText\n        });\n        this.hide();\n        setTimeout(() => this.$text.innerText = \"\", 300);\n    }\n}\n\nclass ReceiveTextDialog extends Dialog {\n    constructor() {\n        super('receive-text-dialog');\n        Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));\n        this.$text = this.$el.querySelector('#text');\n        this.$copy = this.$el.querySelector('#copy');\n        this.$close = this.$el.querySelector('#close');\n\n        this.$copy.addEventListener('click', _ => this._onCopy());\n        this.$close.addEventListener('click', _ => this.hide());\n\n        Events.on('keydown', e => this._onKeyDown(e));\n\n        this.$displayName = this.$el.querySelector('.display-name');\n        this._receiveTextQueue = [];\n        this._hideTimeout = null;\n    }\n\n    selectionEmpty() {\n        return !window.getSelection().toString()\n    }\n\n    async _onKeyDown(e) {\n        if (!this.isShown()) return\n\n        if (e.code === \"KeyC\" && (e.ctrlKey || e.metaKey) && this.selectionEmpty()) {\n            await this._onCopy()\n        }\n        else if (e.code === \"Escape\") {\n            this.hide();\n        }\n    }\n\n    _onText(text, peerId) {\n        window.blop.play();\n        this._receiveTextQueue.push({text: text, peerId: peerId});\n        this._setDocumentTitleMessages();\n        changeFavicon(\"images/favicon-96x96-notification.png\");\n\n        if (this.isShown() || this._hideTimeout) return;\n\n        this._dequeueRequests();\n    }\n\n    _dequeueRequests() {\n        this._setDocumentTitleMessages();\n        changeFavicon(\"images/favicon-96x96-notification.png\");\n\n        let {text, peerId} = this._receiveTextQueue.shift();\n        this._showReceiveTextDialog(text, peerId);\n    }\n\n    _showReceiveTextDialog(text, peerId) {\n        this.$displayName.innerText = $(peerId).ui._displayName();\n        this.$displayName.classList.remove(\"badge-room-ip\", \"badge-room-secret\", \"badge-room-public-id\");\n        this.$displayName.classList.add($(peerId).ui._badgeClassName());\n\n        this.$text.innerText = text;\n\n        // Beautify text if text is not too long\n        if (this.$text.innerText.length <= 300000) {\n            // Hacky workaround to replace URLs with link nodes in all cases\n            // 1. Use text variable, find all valid URLs via regex and replace URLs with placeholder\n            // 2. Use html variable, find placeholders with regex and replace them with link nodes\n\n            let $textShadow = document.createElement('div');\n            $textShadow.innerText = text;\n\n            let linkNodes = {};\n            let searchHTML = $textShadow.innerHTML;\n            const p = \"@\";\n            const pRgx = new RegExp(`${p}\\\\d+`, 'g');\n            let occP = searchHTML.match(pRgx) || [];\n\n            let m = 0;\n\n            const chrs = `a-zA-Z0-9áàäčçđéèêŋńñóòôöšŧüžæøåëìíîïðùúýþćěłřśţźǎǐǒǔǥǧǩǯəʒâûœÿãõāēīōūăąĉċďĕėęĝğġģĥħĩĭįıĵķĸĺļľņňŏőŕŗŝşťũŭůűųŵŷżאבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ`; // allowed chars in domain names\n            const rgxWhitespace = `(^|\\\\n|\\\\s)`;\n            const rgxScheme = `(https?:\\\\/\\\\/)`\n            const rgxSchemeMail = `(mailto:)`\n            const rgxUserinfo = `(?:(?:[${chrs}.%]*(?::[${chrs}.%]*)?)@)`;\n            const rgxHost = `(?:(?:[${chrs}](?:[${chrs}-]{0,61}[${chrs}])?\\\\.)+[${chrs}][${chrs}-]{0,61}[${chrs}])`;\n            const rgxPort = `(:\\\\d*)`;\n            const rgxPath = `(?:(?:\\\\/[${chrs}\\\\-\\\\._~!$&'\\\\(\\\\)\\\\*\\\\+,;=:@%]*)*)`;\n            const rgxQueryAndFragment = `(\\\\?[${chrs}\\\\-_~:\\\\/#\\\\[\\\\]@!$&'\\\\(\\\\)*+,;=%.]*)`;\n            const rgxUrl = `(${rgxScheme}?${rgxHost}${rgxPort}?${rgxPath}${rgxQueryAndFragment}?)`;\n            const rgxMail = `(${rgxSchemeMail}${rgxUserinfo}${rgxHost})`;\n            const rgxUrlAll = new RegExp(`${rgxWhitespace}${rgxUrl}`, 'g');\n            const rgxMailAll = new RegExp(`${rgxWhitespace}${rgxMail}`, 'g');\n\n            const replaceMatchWithPlaceholder = function(match, whitespace, url, scheme) {\n                let link = url;\n\n                // prefix www.example.com with http scheme to prevent it from being a relative link\n                if (!scheme && link.startsWith('www')) {\n                    link = \"http://\" + link\n                }\n\n                if (!isUrlValid(link)) {\n                    // link is not valid -> do not replace\n                    return match;\n                }\n\n                // link is valid -> replace with link node placeholder\n                // find linkNodePlaceholder that is not yet present in text node\n                m++;\n                while (occP.includes(`${p}${m}`)) {\n                    m++;\n                }\n                let linkNodePlaceholder = `${p}${m}`;\n\n                // add linkNodePlaceholder to text node and save a reference to linkNodes object\n                linkNodes[linkNodePlaceholder] = `<a href=\"${link}\" target=\"_blank\" rel=\"noreferrer\">${url}</a>`;\n                return `${whitespace}${linkNodePlaceholder}`;\n            }\n\n            text = text.replace(rgxUrlAll, replaceMatchWithPlaceholder);\n            $textShadow.innerText = text.replace(rgxMailAll, replaceMatchWithPlaceholder);\n\n\n            this.$text.innerHTML = $textShadow.innerHTML.replace(pRgx,\n                (m) => {\n                    let urlNode = linkNodes[m];\n                    return urlNode ? urlNode : m;\n                });\n        }\n\n        this._evaluateOverflowing(this.$text);\n        this.show();\n    }\n\n    _setDocumentTitleMessages() {\n        document.title = this._receiveTextQueue.length <= 1\n            ? `${ Localization.getTranslation(\"document-titles.message-received\") } - PairDrop`\n            : `${ Localization.getTranslation(\"document-titles.message-received-plural\", null, {count: this._receiveTextQueue.length + 1}) } - PairDrop`;\n    }\n\n    async _onCopy() {\n        const sanitizedText = this.$text.innerText.replace(/\\u00A0/gm, ' ');\n        navigator.clipboard\n            .writeText(sanitizedText)\n            .then(_ => {\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.copied-to-clipboard\"));\n                this.hide();\n            })\n            .catch(_ => {\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.copied-to-clipboard-error\"));\n            });\n    }\n\n    hide() {\n        super.hide();\n\n        // If queue is empty -> clear text field | else -> open next message\n        this._hideTimeout = setTimeout(() => {\n            if (!this._receiveTextQueue.length) {\n                this.$text.innerHTML = \"\";\n            }\n            else {\n                this._dequeueRequests();\n            }\n            this._hideTimeout = null;\n        }, 500);\n    }\n}\n\nclass ShareTextDialog extends Dialog {\n    constructor() {\n        super('share-text-dialog');\n\n        this.$text = this.$el.querySelector('.textarea');\n        this.$approveMsgBtn = this.$el.querySelector('button[type=\"submit\"]');\n        this.$checkbox = this.$el.querySelector('input[type=\"checkbox\"]')\n\n        this.$approveMsgBtn.addEventListener('click', _ => this._approveShareText());\n\n        // Only show this per default if user sets checkmark\n        this.$checkbox.checked = localStorage.getItem('approve-share-text')\n            ? ShareTextDialog.isApproveShareTextSet()\n            : false;\n\n        this._setCheckboxValueToLocalStorage();\n\n        this.$checkbox.addEventListener('change', _ => this._setCheckboxValueToLocalStorage());\n        Events.on('share-text-dialog', e => this._onShareText(e.detail));\n        Events.on('keydown', e => this._onKeyDown(e));\n        this.$text.addEventListener('input', _ => this._evaluateEmptyText());\n    }\n\n    static isApproveShareTextSet() {\n        return localStorage.getItem('approve-share-text') === \"true\";\n    }\n\n    _setCheckboxValueToLocalStorage() {\n        localStorage.setItem('approve-share-text', this.$checkbox.checked ? \"true\" : \"false\");\n    }\n\n    _onKeyDown(e) {\n        if (!this.isShown()) return;\n\n        if (e.code === \"Escape\") {\n            this._approveShareText();\n        }\n        else if (e.code === \"Enter\" && (e.ctrlKey || e.metaKey)) {\n            if (this._textEmpty()) return;\n\n            this._approveShareText();\n        }\n    }\n\n    _textEmpty() {\n        return !this.$text.innerText || this.$text.innerText === \"\\n\";\n    }\n\n    _evaluateEmptyText() {\n        if (this._textEmpty()) {\n            this.$approveMsgBtn.setAttribute('disabled', true);\n            // remove remaining whitespace on Firefox on text deletion\n            this.$text.innerText = \"\";\n        }\n        else {\n            this.$approveMsgBtn.removeAttribute('disabled');\n        }\n        this._evaluateOverflowing(this.$text);\n    }\n\n    _onShareText(text) {\n        this.$text.innerText = text;\n        this._evaluateEmptyText();\n        this.show();\n    }\n\n    _approveShareText() {\n        Events.fire('activate-share-mode', {text: this.$text.innerText});\n        this.hide();\n    }\n\n    hide() {\n        super.hide();\n        setTimeout(() => this.$text.innerText = \"\", 500);\n    }\n}\n\nclass Base64Dialog extends Dialog {\n\n    constructor() {\n        super('base64-paste-dialog');\n\n        this.$title = this.$el.querySelector('.dialog-title');\n        this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');\n        this.$fallbackTextarea = this.$el.querySelector('.textarea');\n    }\n\n    async evaluateBase64Text(base64Text, hash) {\n        this.$title.innerText = Localization.getTranslation('dialogs.base64-title-text');\n\n        if (base64Text === 'paste') {\n            // ?base64text=paste\n            // base64 encoded string is ready to be pasted from clipboard\n            this.preparePasting('text');\n            this.show();\n        }\n        else if (base64Text === 'hash') {\n            // ?base64text=hash#BASE64ENCODED\n            // base64 encoded text is url hash which cannot be seen by the server and is faster (recommended)\n            this.show();\n            await this.processBase64Text(hash);\n        }\n        else {\n            // ?base64text=BASE64ENCODED\n            // base64 encoded text is part of the url param. Seen by server and slow (not recommended)\n            this.show();\n            await this.processBase64Text(base64Text);\n        }\n    }\n\n    async evaluateBase64Zip(base64Zip, hash) {\n        this.$title.innerText = Localization.getTranslation('dialogs.base64-title-files');\n\n        if (base64Zip === 'paste') {\n            // ?base64zip=paste || ?base64zip=true\n            this.preparePasting('files');\n            this.show();\n        }\n        else if (base64Zip === 'hash') {\n            // ?base64zip=hash#BASE64ENCODED\n            // base64 encoded zip file is url hash which cannot be seen by the server\n            await this.processBase64Zip(hash);\n        }\n    }\n\n    _setPasteBtnToProcessing() {\n        this.$pasteBtn.style.pointerEvents = \"none\";\n        this.$pasteBtn.innerText = Localization.getTranslation(\"dialogs.base64-processing\");\n    }\n\n    preparePasting(type) {\n        const translateType = type === 'text'\n            ? Localization.getTranslation(\"dialogs.base64-text\")\n            : Localization.getTranslation(\"dialogs.base64-files\");\n\n        if (navigator.clipboard.readText) {\n            this.$pasteBtn.innerText = Localization.getTranslation(\"dialogs.base64-tap-to-paste\", null, {type: translateType});\n            this._clickCallback = _ => this.processClipboard(type);\n            this.$pasteBtn.addEventListener('click', _ => this._clickCallback());\n        }\n        else {\n            console.log(\"`navigator.clipboard.readText()` is not available on your browser.\\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.\")\n            this.$pasteBtn.setAttribute('hidden', true);\n            this.$fallbackTextarea.setAttribute('placeholder', Localization.getTranslation(\"dialogs.base64-paste-to-send\", null, {type: translateType}));\n            this.$fallbackTextarea.removeAttribute('hidden');\n            this._inputCallback = _ => this.processInput(type);\n            this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());\n            this.$fallbackTextarea.focus();\n        }\n    }\n\n    async processInput(type) {\n        const base64 = this.$fallbackTextarea.textContent;\n        this.$fallbackTextarea.textContent = '';\n        await this.processPastedBase64(type, base64);\n    }\n\n    async processClipboard(type) {\n        const base64 = await navigator.clipboard.readText();\n        await this.processPastedBase64(type, base64);\n    }\n\n    async processPastedBase64(type, base64) {\n        try {\n            if (type === 'text') {\n                await this.processBase64Text(base64);\n            }\n            else {\n                await this.processBase64Zip(base64);\n            }\n        }\n        catch(e) {\n            Events.fire('notify-user', Localization.getTranslation(\"notifications.clipboard-content-incorrect\"));\n            console.log(\"Clipboard content is incorrect.\")\n        }\n        this.hide();\n    }\n\n    async processBase64Text(base64){\n        this._setPasteBtnToProcessing();\n\n        try {\n            const decodedText = await decodeBase64Text(base64);\n            if (ShareTextDialog.isApproveShareTextSet()) {\n                Events.fire('share-text-dialog', decodedText);\n            }\n            else {\n                Events.fire('activate-share-mode', {text: decodedText});\n            }\n        }\n        catch (e) {\n            Events.fire('notify-user', Localization.getTranslation(\"notifications.text-content-incorrect\"));\n            console.log(\"Text content incorrect.\");\n        }\n\n        this.hide();\n    }\n\n    async processBase64Zip(base64) {\n        this._setPasteBtnToProcessing();\n\n        try {\n            const decodedFiles = await decodeBase64Files(base64);\n            Events.fire('activate-share-mode', {files: decodedFiles});\n        }\n        catch (e) {\n            Events.fire('notify-user', Localization.getTranslation(\"notifications.file-content-incorrect\"));\n            console.log(\"File content incorrect.\");\n        }\n\n        this.hide();\n    }\n\n    hide() {\n        this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());\n        this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());\n        this.$fallbackTextarea.setAttribute('disabled', true);\n        this.$fallbackTextarea.blur();\n        super.hide();\n    }\n}\n\nclass AboutUI {\n    constructor() {\n        this.$donationBtn = $('donation-btn');\n        this.$twitterBtn = $('x-twitter-btn');\n        this.$mastodonBtn = $('mastodon-btn');\n        this.$blueskyBtn = $('bluesky-btn');\n        this.$customBtn = $('custom-btn');\n        this.$privacypolicyBtn = $('privacypolicy-btn');\n        Events.on('config', e => this._onConfig(e.detail.buttons));\n    }\n\n    async _onConfig(btnConfig) {\n        await this._evaluateBtnConfig(this.$donationBtn, btnConfig.donation_button);\n        await this._evaluateBtnConfig(this.$twitterBtn, btnConfig.twitter_button);\n        await this._evaluateBtnConfig(this.$mastodonBtn, btnConfig.mastodon_button);\n        await this._evaluateBtnConfig(this.$blueskyBtn, btnConfig.bluesky_button);\n        await this._evaluateBtnConfig(this.$customBtn, btnConfig.custom_button);\n        await this._evaluateBtnConfig(this.$privacypolicyBtn, btnConfig.privacypolicy_button);\n    }\n\n    async _evaluateBtnConfig($btn, config) {\n        // if config is not set leave everything as default\n        if (!Object.keys(config).length) return;\n\n        if (config.active === \"false\") {\n            $btn.setAttribute('hidden', true);\n        } else {\n            if (config.link) {\n                $btn.setAttribute('href', config.link);\n            }\n            if (config.title) {\n                $btn.setAttribute('title', config.title);\n                // prevent overwriting of custom title when setting different language\n                $btn.removeAttribute('data-i18n-key');\n                $btn.removeAttribute('data-i18n-attrs');\n            }\n            if (config.icon) {\n                $btn.setAttribute('title', config.title);\n                // prevent overwriting of custom title when setting different language\n                $btn.removeAttribute('data-i18n-key');\n                $btn.removeAttribute('data-i18n-attrs');\n            }\n            $btn.removeAttribute('hidden');\n        }\n    }\n}\n\nclass Toast extends Dialog {\n    constructor() {\n        super('toast');\n        this.$closeBtn = this.$el.querySelector('.icon-button');\n        this.$text = this.$el.querySelector('span');\n\n        this.$closeBtn.addEventListener('click', _ => this.hide());\n        Events.on('notify-user', e => this._onNotify(e.detail));\n        Events.on('share-mode-changed', _ => this.hide());\n    }\n\n    _onNotify(message) {\n        if (this.hideTimeout) clearTimeout(this.hideTimeout);\n        this.$text.innerText = typeof message === \"object\" ? message.message : message;\n        this.show();\n\n        if (typeof message === \"object\" && message.persistent) return;\n\n        this.hideTimeout = setTimeout(() => this.hide(), 5000);\n    }\n\n    hide() {\n        if (this.hideTimeout) clearTimeout(this.hideTimeout);\n        super.hide();\n    }\n}\n\nclass Notifications {\n\n    constructor() {\n        // Check if the browser supports notifications\n        if (!('Notification' in window)) return;\n\n        this.$headerNotificationButton = $('notification');\n        this.$downloadBtn = $('download-btn');\n\n        this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission());\n\n\n        Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));\n        Events.on('files-received', e => this._downloadNotification(e.detail.files));\n        Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));\n    }\n\n    async _requestPermission() {\n        await Notification.\n            requestPermission(permission => {\n                if (permission !== 'granted') {\n                    Events.fire('notify-user', Localization.getTranslation(\"notifications.notifications-permissions-error\"));\n                    return;\n                }\n                Events.fire('notify-user', Localization.getTranslation(\"notifications.notifications-enabled\"));\n                this.$headerNotificationButton.setAttribute('hidden', true);\n            });\n    }\n\n    _notify(title, body) {\n        const config = {\n            body: body,\n            icon: '/images/logo_transparent_128x128.png',\n        }\n        let notification;\n        try {\n            notification = new Notification(title, config);\n        } catch (e) {\n            // Android doesn't support \"new Notification\" if service worker is installed\n            if (!serviceWorker || !serviceWorker.showNotification) return;\n            notification = serviceWorker.showNotification(title, config);\n        }\n\n        // Notification is persistent on Android. We have to close it manually\n        const visibilitychangeHandler = () => {\n            if (document.visibilityState === 'visible') {\n                notification.close();\n                Events.off('visibilitychange', visibilitychangeHandler);\n            }\n        };\n        Events.on('visibilitychange', visibilitychangeHandler);\n\n        return notification;\n    }\n\n    _messageNotification(message, peerId) {\n        if (document.visibilityState !== 'visible') {\n            const peerDisplayName = $(peerId).ui._displayName();\n            if (/^((https?:\\/\\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\\-._~:\\/?#\\[\\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {\n                const notification = this._notify(Localization.getTranslation(\"notifications.link-received\", null, {name: peerDisplayName}), message);\n                this._bind(notification, _ => window.open(message, '_blank', \"noreferrer\"));\n            }\n            else {\n                const notification = this._notify(Localization.getTranslation(\"notifications.message-received\", null, {name: peerDisplayName}), message);\n                this._bind(notification, _ => this._copyText(message, notification));\n            }\n        }\n    }\n\n    _downloadNotification(files) {\n        if (document.visibilityState !== 'visible') {\n            let imagesOnly = files.every(file => file.type.split('/')[0] === 'image');\n            let title;\n\n            if (files.length === 1) {\n                title = `${files[0].name}`;\n            }\n            else {\n                let fileOther;\n                if (files.length === 2) {\n                    fileOther = imagesOnly\n                        ? Localization.getTranslation(\"dialogs.file-other-description-image\")\n                        : Localization.getTranslation(\"dialogs.file-other-description-file\");\n                }\n                else {\n                    fileOther = imagesOnly\n                        ? Localization.getTranslation(\"dialogs.file-other-description-image-plural\", null, {count: files.length - 1})\n                        : Localization.getTranslation(\"dialogs.file-other-description-file-plural\", null, {count: files.length - 1});\n                }\n                title = `${files[0].name} ${fileOther}`\n            }\n            const notification = this._notify(title, Localization.getTranslation(\"notifications.click-to-download\"));\n            this._bind(notification, _ => this._download(notification));\n        }\n    }\n\n    _requestNotification(request, peerId) {\n        if (document.visibilityState !== 'visible') {\n            let imagesOnly = request.header.every(header => header.mime.split('/')[0] === 'image');\n            let displayName = $(peerId).querySelector('.name').textContent;\n\n            let descriptor;\n            if (request.header.length === 1) {\n                descriptor = imagesOnly\n                    ? Localization.getTranslation(\"dialogs.title-image\")\n                    : Localization.getTranslation(\"dialogs.title-file\");\n            }\n            else {\n                descriptor = imagesOnly\n                    ? Localization.getTranslation(\"dialogs.title-image-plural\")\n                    : Localization.getTranslation(\"dialogs.title-file-plural\");\n            }\n\n            let title = Localization\n                .getTranslation(\"notifications.request-title\", null, {\n                    name: displayName,\n                    count: request.header.length,\n                    descriptor: descriptor.toLowerCase()\n                });\n\n            const notification = this._notify(title, Localization.getTranslation(\"notifications.click-to-show\"));\n        }\n    }\n\n    _download(notification) {\n        this.$downloadBtn.click();\n        notification.close();\n    }\n\n    async _copyText(message, notification) {\n        if (await navigator.clipboard.writeText(message)) {\n            notification.close();\n            this._notify(Localization.getTranslation(\"notifications.copied-text\"));\n        }\n        else {\n            this._notify(Localization.getTranslation(\"notifications.copied-text-error\"));\n        }\n    }\n\n    _bind(notification, handler) {\n        if (notification.then) {\n            notification.then(_ => {\n                serviceWorker\n                    .getNotifications()\n                    .then(_ => {\n                        serviceWorker.addEventListener('notificationclick', handler);\n                    })\n            });\n        }\n        else {\n            notification.onclick = handler;\n        }\n    }\n}\n\nclass NetworkStatusUI {\n\n    constructor() {\n        Events.on('offline', _ => this._showOfflineMessage());\n        Events.on('online', _ => this._showOnlineMessage());\n        if (!navigator.onLine) this._showOfflineMessage();\n    }\n\n    _showOfflineMessage() {\n        Events.fire('notify-user', {\n            message: Localization.getTranslation(\"notifications.offline\"),\n            persistent: true\n        });\n    }\n\n    _showOnlineMessage() {\n        Events.fire('notify-user', Localization.getTranslation(\"notifications.online\"));\n    }\n}\n\nclass WebShareTargetUI {\n\n    async evaluateShareTarget(shareTargetType, title, text, url) {\n        if (shareTargetType === \"text\") {\n            let shareTargetText;\n            if (url) {\n                shareTargetText = url; // we share only the link - no text.\n            }\n            else if (title && text) {\n                shareTargetText = title + '\\r\\n' + text;\n            }\n            else {\n                shareTargetText = title + text;\n            }\n\n            if (ShareTextDialog.isApproveShareTextSet()) {\n                Events.fire('share-text-dialog', shareTargetText);\n            }\n            else {\n                Events.fire('activate-share-mode', {text: shareTargetText});\n            }\n        }\n        else if (shareTargetType === \"files\") {\n            let openRequest = window.indexedDB.open('pairdrop_store')\n            openRequest.onsuccess = e => {\n                const db = e.target.result;\n                const tx = db.transaction('share_target_files', 'readwrite');\n                const store = tx.objectStore('share_target_files');\n                const request = store.getAll();\n                request.onsuccess = _ => {\n                    const fileObjects = request.result;\n\n                    let filesReceived = [];\n                    for (let i = 0; i < fileObjects.length; i++) {\n                        filesReceived.push(new File([fileObjects[i].buffer], fileObjects[i].name));\n                    }\n\n                    const clearRequest = store.clear()\n                    clearRequest.onsuccess = _ => db.close();\n\n                    Events.fire('activate-share-mode', {files: filesReceived})\n                }\n            }\n        }\n    }\n}\n\n// Keep for legacy reasons even though this is removed from new PWA installations\nclass WebFileHandlersUI {\n    async evaluateLaunchQueue() {\n        if (!\"launchQueue\" in window) return;\n\n        launchQueue.setConsumer(async launchParams => {\n            console.log(\"Launched with: \", launchParams);\n\n            if (!launchParams.files.length) return;\n\n            let files = [];\n\n            for (let i = 0; i < launchParams.files.length; i++) {\n                if (i !== 0 && await launchParams.files[i].isSameEntry(launchParams.files[i-1])) continue;\n\n                const file = await launchParams.files[i].getFile();\n                files.push(file);\n            }\n\n            Events.fire('activate-share-mode', {files: files})\n        });\n    }\n}\n\nclass NoSleepUI {\n    constructor() {\n        NoSleepUI._nosleep = new NoSleep();\n    }\n\n    static enable() {\n        if (!this._interval) {\n            NoSleepUI._nosleep.enable();\n            NoSleepUI._interval = setInterval(() => NoSleepUI.disable(), 10000);\n        }\n    }\n\n    static disable() {\n        if ($$('x-peer[status]') === null) {\n            clearInterval(NoSleepUI._interval);\n            NoSleepUI._nosleep.disable();\n        }\n    }\n}\n"
  },
  {
    "path": "public/scripts/util.js",
    "content": "// Polyfill for Navigator.clipboard.writeText\nif (!navigator.clipboard) {\n    navigator.clipboard = {\n        writeText: text => {\n\n            // A <span> contains the text to copy\n            const span = document.createElement('span');\n            span.innerText = text;\n            span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines\n\n            // Paint the span outside the viewport\n            span.style.position = 'absolute';\n            span.style.left = '-9999px';\n            span.style.top = '-9999px';\n\n            const win = window;\n            const selection = win.getSelection();\n            win.document.body.appendChild(span);\n\n            const range = win.document.createRange();\n            selection.removeAllRanges();\n            range.selectNode(span);\n            selection.addRange(range);\n\n            let success = false;\n            try {\n                success = win.document.execCommand('copy');\n            } catch (err) {\n                return Promise.error();\n            }\n\n            selection.removeAllRanges();\n            span.remove();\n\n            return Promise.resolve();\n        }\n    }\n}\n\n// Polyfills\nwindow.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);\n\nwindow.hiddenProperty = 'hidden' in document\n    ? 'hidden'\n    : 'webkitHidden' in document\n        ? 'webkitHidden'\n        : 'mozHidden' in document\n            ? 'mozHidden'\n            : null;\n\nwindow.visibilityChangeEvent = 'visibilitychange' in document\n    ? 'visibilitychange'\n    : 'webkitvisibilitychange' in document\n        ? 'webkitvisibilitychange'\n        : 'mozvisibilitychange' in document\n            ? 'mozvisibilitychange'\n            : null;\n\nwindow.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;\nwindow.android = /android/i.test(navigator.userAgent);\nwindow.isMobile = window.iOS || window.android;\n\n\n// Helper functions\nconst zipper = (() => {\n\n    let zipWriter;\n    return {\n        createNewZipWriter() {\n            zipWriter = new zip.ZipWriter(new zip.BlobWriter(\"application/zip\"), { bufferedWrite: true, level: 0 });\n        },\n        addFile(file, options) {\n            return zipWriter.add(file.name, new zip.BlobReader(file), options);\n        },\n        async getBlobURL() {\n            if (zipWriter) {\n                const blobURL = URL.createObjectURL(await zipWriter.close());\n                zipWriter = null;\n                return blobURL;\n            }\n            else {\n                throw new Error(\"Zip file closed\");\n            }\n        },\n        async getZipFile(filename = \"archive.zip\") {\n            if (zipWriter) {\n                const file = new File([await zipWriter.close()], filename, {type: \"application/zip\"});\n                zipWriter = null;\n                return file;\n            }\n            else {\n                throw new Error(\"Zip file closed\");\n            }\n        },\n        async getEntries(file, options) {\n            return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options);\n        },\n        async getData(entry, options) {\n            return await entry.getData(new zip.BlobWriter(), options);\n        },\n    };\n\n})();\n\nconst mime = (() => {\n\n    const suffixToMimeMap = {\n        \"cpl\": \"application/cpl+xml\",\n        \"gpx\": \"application/gpx+xml\",\n        \"gz\": \"application/gzip\",\n        \"jar\": \"application/java-archive\",\n        \"war\": \"application/java-archive\",\n        \"ear\": \"application/java-archive\",\n        \"class\": \"application/java-vm\",\n        \"js\": \"application/javascript\",\n        \"mjs\": \"application/javascript\",\n        \"json\": \"application/json\",\n        \"map\": \"application/json\",\n        \"webmanifest\": \"application/manifest+json\",\n        \"doc\": \"application/msword\",\n        \"dot\": \"application/msword\",\n        \"wiz\": \"application/msword\",\n        \"bin\": \"application/octet-stream\",\n        \"dms\": \"application/octet-stream\",\n        \"lrf\": \"application/octet-stream\",\n        \"mar\": \"application/octet-stream\",\n        \"so\": \"application/octet-stream\",\n        \"dist\": \"application/octet-stream\",\n        \"distz\": \"application/octet-stream\",\n        \"pkg\": \"application/octet-stream\",\n        \"bpk\": \"application/octet-stream\",\n        \"dump\": \"application/octet-stream\",\n        \"elc\": \"application/octet-stream\",\n        \"deploy\": \"application/octet-stream\",\n        \"img\": \"application/octet-stream\",\n        \"msp\": \"application/octet-stream\",\n        \"msm\": \"application/octet-stream\",\n        \"buffer\": \"application/octet-stream\",\n        \"oda\": \"application/oda\",\n        \"oxps\": \"application/oxps\",\n        \"pdf\": \"application/pdf\",\n        \"asc\": \"application/pgp-signature\",\n        \"sig\": \"application/pgp-signature\",\n        \"prf\": \"application/pics-rules\",\n        \"p7c\": \"application/pkcs7-mime\",\n        \"cer\": \"application/pkix-cert\",\n        \"ai\": \"application/postscript\",\n        \"eps\": \"application/postscript\",\n        \"ps\": \"application/postscript\",\n        \"apk\": \"application/vnd.android.package-archive\",\n        \"m3u8\": \"application/vnd.apple.mpegurl\",\n        \"pkpass\": \"application/vnd.apple.pkpass\",\n        \"kml\": \"application/vnd.google-earth.kml+xml\",\n        \"kmz\": \"application/vnd.google-earth.kmz\",\n        \"cab\": \"application/vnd.ms-cab-compressed\",\n        \"xls\": \"application/vnd.ms-excel\",\n        \"xlm\": \"application/vnd.ms-excel\",\n        \"xla\": \"application/vnd.ms-excel\",\n        \"xlc\": \"application/vnd.ms-excel\",\n        \"xlt\": \"application/vnd.ms-excel\",\n        \"xlw\": \"application/vnd.ms-excel\",\n        \"msg\": \"application/vnd.ms-outlook\",\n        \"ppt\": \"application/vnd.ms-powerpoint\",\n        \"pot\": \"application/vnd.ms-powerpoint\",\n        \"ppa\": \"application/vnd.ms-powerpoint\",\n        \"pps\": \"application/vnd.ms-powerpoint\",\n        \"pwz\": \"application/vnd.ms-powerpoint\",\n        \"mpp\": \"application/vnd.ms-project\",\n        \"mpt\": \"application/vnd.ms-project\",\n        \"xps\": \"application/vnd.ms-xpsdocument\",\n        \"odb\": \"application/vnd.oasis.opendocument.database\",\n        \"ods\": \"application/vnd.oasis.opendocument.spreadsheet\",\n        \"odt\": \"application/vnd.oasis.opendocument.text\",\n        \"osm\": \"application/vnd.openstreetmap.data+xml\",\n        \"pptx\": \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n        \"xlsx\": \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n        \"docx\": \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n        \"pcap\": \"application/vnd.tcpdump.pcap\",\n        \"cap\": \"application/vnd.tcpdump.pcap\",\n        \"dmp\": \"application/vnd.tcpdump.pcap\",\n        \"wpd\": \"application/vnd.wordperfect\",\n        \"wasm\": \"application/wasm\",\n        \"7z\": \"application/x-7z-compressed\",\n        \"dmg\": \"application/x-apple-diskimage\",\n        \"bcpio\": \"application/x-bcpio\",\n        \"torrent\": \"application/x-bittorrent\",\n        \"cbr\": \"application/x-cbr\",\n        \"cba\": \"application/x-cbr\",\n        \"cbt\": \"application/x-cbr\",\n        \"cbz\": \"application/x-cbr\",\n        \"cb7\": \"application/x-cbr\",\n        \"vcd\": \"application/x-cdlink\",\n        \"crx\": \"application/x-chrome-extension\",\n        \"cpio\": \"application/x-cpio\",\n        \"csh\": \"application/x-csh\",\n        \"deb\": \"application/x-debian-package\",\n        \"udeb\": \"application/x-debian-package\",\n        \"dvi\": \"application/x-dvi\",\n        \"arc\": \"application/x-freearc\",\n        \"gtar\": \"application/x-gtar\",\n        \"hdf\": \"application/x-hdf\",\n        \"h5\": \"application/x-hdf5\",\n        \"php\": \"application/x-httpd-php\",\n        \"iso\": \"application/x-iso9660-image\",\n        \"key\": \"application/x-iwork-keynote-sffkey\",\n        \"numbers\": \"application/x-iwork-numbers-sffnumbers\",\n        \"pages\": \"application/x-iwork-pages-sffpages\",\n        \"latex\": \"application/x-latex\",\n        \"run\": \"application/x-makeself\",\n        \"mif\": \"application/x-mif\",\n        \"lnk\": \"application/x-ms-shortcut\",\n        \"mdb\": \"application/x-msaccess\",\n        \"exe\": \"application/x-msdownload\",\n        \"dll\": \"application/x-msdownload\",\n        \"com\": \"application/x-msdownload\",\n        \"bat\": \"application/x-msdownload\",\n        \"msi\": \"application/x-msdownload\",\n        \"pub\": \"application/x-mspublisher\",\n        \"cdf\": \"application/x-netcdf\",\n        \"nc\": \"application/x-netcdf\",\n        \"pl\": \"application/x-perl\",\n        \"pm\": \"application/x-perl\",\n        \"prc\": \"application/x-pilot\",\n        \"pdb\": \"application/x-pilot\",\n        \"p12\": \"application/x-pkcs12\",\n        \"pfx\": \"application/x-pkcs12\",\n        \"ram\": \"application/x-pn-realaudio\",\n        \"pyc\": \"application/x-python-code\",\n        \"pyo\": \"application/x-python-code\",\n        \"rar\": \"application/x-rar-compressed\",\n        \"rpm\": \"application/x-redhat-package-manager\",\n        \"sh\": \"application/x-sh\",\n        \"shar\": \"application/x-shar\",\n        \"swf\": \"application/x-shockwave-flash\",\n        \"sql\": \"application/x-sql\",\n        \"srt\": \"application/x-subrip\",\n        \"sv4cpio\": \"application/x-sv4cpio\",\n        \"sv4crc\": \"application/x-sv4crc\",\n        \"gam\": \"application/x-tads\",\n        \"tar\": \"application/x-tar\",\n        \"tcl\": \"application/x-tcl\",\n        \"tex\": \"application/x-tex\",\n        \"roff\": \"application/x-troff\",\n        \"t\": \"application/x-troff\",\n        \"tr\": \"application/x-troff\",\n        \"man\": \"application/x-troff-man\",\n        \"me\": \"application/x-troff-me\",\n        \"ms\": \"application/x-troff-ms\",\n        \"ustar\": \"application/x-ustar\",\n        \"src\": \"application/x-wais-source\",\n        \"xpi\": \"application/x-xpinstall\",\n        \"xhtml\": \"application/xhtml+xml\",\n        \"xht\": \"application/xhtml+xml\",\n        \"xsl\": \"application/xml\",\n        \"rdf\": \"application/xml\",\n        \"wsdl\": \"application/xml\",\n        \"xpdl\": \"application/xml\",\n        \"zip\": \"application/zip\",\n        \"3gp\": \"audio/3gp\",\n        \"3gpp\": \"audio/3gpp\",\n        \"3g2\": \"audio/3gpp2\",\n        \"3gpp2\": \"audio/3gpp2\",\n        \"aac\": \"audio/aac\",\n        \"adts\": \"audio/aac\",\n        \"loas\": \"audio/aac\",\n        \"ass\": \"audio/aac\",\n        \"au\": \"audio/basic\",\n        \"snd\": \"audio/basic\",\n        \"mid\": \"audio/midi\",\n        \"midi\": \"audio/midi\",\n        \"kar\": \"audio/midi\",\n        \"rmi\": \"audio/midi\",\n        \"mpga\": \"audio/mpeg\",\n        \"mp2\": \"audio/mpeg\",\n        \"mp2a\": \"audio/mpeg\",\n        \"mp3\": \"audio/mpeg\",\n        \"m2a\": \"audio/mpeg\",\n        \"m3a\": \"audio/mpeg\",\n        \"oga\": \"audio/ogg\",\n        \"ogg\": \"audio/ogg\",\n        \"spx\": \"audio/ogg\",\n        \"opus\": \"audio/opus\",\n        \"aif\": \"audio/x-aiff\",\n        \"aifc\": \"audio/x-aiff\",\n        \"aiff\": \"audio/x-aiff\",\n        \"flac\": \"audio/x-flac\",\n        \"m4a\": \"audio/x-m4a\",\n        \"m3u\": \"audio/x-mpegurl\",\n        \"wma\": \"audio/x-ms-wma\",\n        \"ra\": \"audio/x-pn-realaudio\",\n        \"wav\": \"audio/x-wav\",\n        \"otf\": \"font/otf\",\n        \"ttf\": \"font/ttf\",\n        \"woff\": \"font/woff\",\n        \"woff2\": \"font/woff2\",\n        \"emf\": \"image/emf\",\n        \"gif\": \"image/gif\",\n        \"heic\": \"image/heic\",\n        \"heif\": \"image/heif\",\n        \"ief\": \"image/ief\",\n        \"jpeg\": \"image/jpeg\",\n        \"jpg\": \"image/jpeg\",\n        \"pict\": \"image/pict\",\n        \"pct\": \"image/pict\",\n        \"pic\": \"image/pict\",\n        \"png\": \"image/png\",\n        \"svg\": \"image/svg+xml\",\n        \"svgz\": \"image/svg+xml\",\n        \"tif\": \"image/tiff\",\n        \"tiff\": \"image/tiff\",\n        \"psd\": \"image/vnd.adobe.photoshop\",\n        \"djvu\": \"image/vnd.djvu\",\n        \"djv\": \"image/vnd.djvu\",\n        \"dwg\": \"image/vnd.dwg\",\n        \"dxf\": \"image/vnd.dxf\",\n        \"dds\": \"image/vnd.ms-dds\",\n        \"webp\": \"image/webp\",\n        \"3ds\": \"image/x-3ds\",\n        \"ras\": \"image/x-cmu-raster\",\n        \"ico\": \"image/x-icon\",\n        \"bmp\": \"image/x-ms-bmp\",\n        \"pnm\": \"image/x-portable-anymap\",\n        \"pbm\": \"image/x-portable-bitmap\",\n        \"pgm\": \"image/x-portable-graymap\",\n        \"ppm\": \"image/x-portable-pixmap\",\n        \"rgb\": \"image/x-rgb\",\n        \"tga\": \"image/x-tga\",\n        \"xbm\": \"image/x-xbitmap\",\n        \"xpm\": \"image/x-xpixmap\",\n        \"xwd\": \"image/x-xwindowdump\",\n        \"eml\": \"message/rfc822\",\n        \"mht\": \"message/rfc822\",\n        \"mhtml\": \"message/rfc822\",\n        \"nws\": \"message/rfc822\",\n        \"obj\": \"model/obj\",\n        \"stl\": \"model/stl\",\n        \"dae\": \"model/vnd.collada+xml\",\n        \"ics\": \"text/calendar\",\n        \"ifb\": \"text/calendar\",\n        \"css\": \"text/css\",\n        \"csv\": \"text/csv\",\n        \"html\": \"text/html\",\n        \"htm\": \"text/html\",\n        \"shtml\": \"text/html\",\n        \"markdown\": \"text/markdown\",\n        \"md\": \"text/markdown\",\n        \"txt\": \"text/plain\",\n        \"text\": \"text/plain\",\n        \"conf\": \"text/plain\",\n        \"def\": \"text/plain\",\n        \"list\": \"text/plain\",\n        \"log\": \"text/plain\",\n        \"in\": \"text/plain\",\n        \"ini\": \"text/plain\",\n        \"rtx\": \"text/richtext\",\n        \"rtf\": \"text/rtf\",\n        \"tsv\": \"text/tab-separated-values\",\n        \"c\": \"text/x-c\",\n        \"cc\": \"text/x-c\",\n        \"cxx\": \"text/x-c\",\n        \"cpp\": \"text/x-c\",\n        \"h\": \"text/x-c\",\n        \"hh\": \"text/x-c\",\n        \"dic\": \"text/x-c\",\n        \"java\": \"text/x-java-source\",\n        \"lua\": \"text/x-lua\",\n        \"py\": \"text/x-python\",\n        \"etx\": \"text/x-setext\",\n        \"sgm\": \"text/x-sgml\",\n        \"sgml\": \"text/x-sgml\",\n        \"vcf\": \"text/x-vcard\",\n        \"xml\": \"text/xml\",\n        \"xul\": \"text/xul\",\n        \"yaml\": \"text/yaml\",\n        \"yml\": \"text/yaml\",\n        \"ts\": \"video/mp2t\",\n        \"mp4\": \"video/mp4\",\n        \"mp4v\": \"video/mp4\",\n        \"mpg4\": \"video/mp4\",\n        \"mpeg\": \"video/mpeg\",\n        \"m1v\": \"video/mpeg\",\n        \"mpa\": \"video/mpeg\",\n        \"mpe\": \"video/mpeg\",\n        \"mpg\": \"video/mpeg\",\n        \"mov\": \"video/quicktime\",\n        \"qt\": \"video/quicktime\",\n        \"webm\": \"video/webm\",\n        \"flv\": \"video/x-flv\",\n        \"m4v\": \"video/x-m4v\",\n        \"asf\": \"video/x-ms-asf\",\n        \"asx\": \"video/x-ms-asf\",\n        \"vob\": \"video/x-ms-vob\",\n        \"wmv\": \"video/x-ms-wmv\",\n        \"avi\": \"video/x-msvideo\",\n        \"*\": \"video/x-sgi-movie\",\n        \"kdbx\": \"application/x-keepass2\"\n    }\n\n    return {\n        guessMimeByFilename(filename) {\n            const split = filename.split('.');\n            if (split.length === 1) {\n                // Filename does not include suffix\n                return false;\n            }\n            const suffix = split[split.length - 1].toLowerCase();\n            return suffixToMimeMap[suffix];\n        },\n        addMissingMimeTypesToFiles(files) {\n            // if filetype is empty guess via suffix otherwise leave unchanged\n            for (let i = 0; i < files.length; i++) {\n                if (!files[i].type) {\n                    files[i] = new File([files[i]], files[i].name, {type: mime.guessMimeByFilename(files[i].name) || \"application/octet-stream\"});\n                }\n            }\n            return files;\n        }\n    };\n\n})();\n\n/*\n    cyrb53 (c) 2018 bryc (github.com/bryc)\n    A fast and simple hash function with decent collision resistance.\n    Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.\n    Public domain. Attribution appreciated.\n*/\nconst cyrb53 = function(str, seed = 0) {\n    let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;\n    for (let i = 0, ch; i < str.length; i++) {\n        ch = str.charCodeAt(i);\n        h1 = Math.imul(h1 ^ ch, 2654435761);\n        h2 = Math.imul(h2 ^ ch, 1597334677);\n    }\n    h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);\n    h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);\n    return 4294967296 * (2097151 & h2) + (h1>>>0);\n};\n\nfunction onlyUnique (value, index, array) {\n    return array.indexOf(value) === index;\n}\n\nfunction getUrlWithoutArguments() {\n    return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;\n}\n\nfunction changeFavicon(src) {\n    document.querySelector('[rel=\"icon\"]').href = src;\n    document.querySelector('[rel=\"shortcut icon\"]').href = src;\n}\n\nfunction arrayBufferToBase64(buffer) {\n    let binary = '';\n    let bytes = new Uint8Array(buffer);\n    let len = bytes.byteLength;\n    for (let i = 0; i < len; i++) {\n        binary += String.fromCharCode(bytes[i]);\n    }\n    return window.btoa( binary );\n}\n\nfunction base64ToArrayBuffer(base64) {\n    let binary_string = window.atob(base64);\n    let len = binary_string.length;\n    let bytes = new Uint8Array(len);\n    for (let i = 0; i < len; i++) {\n        bytes[i] = binary_string.charCodeAt(i);\n    }\n    return bytes.buffer;\n}\n\nasync function fileToBlob (file) {\n    return new Blob([new Uint8Array(await file.arrayBuffer())], {type: file.type});\n}\n\nfunction getThumbnailAsDataUrl(file, width = undefined, height = undefined, quality = 0.7) {\n    return new Promise(async (resolve, reject) => {\n        try {\n            if (file.type === \"image/heif\" || file.type === \"image/heic\") {\n                // hotfix: Converting heic images taken on iOS 18 crashes page. Waiting for PR #350\n                reject(new Error(`Hotfix: Converting of HEIC/HEIF images currently disabled.`));\n                return;\n                // // browsers can't show heic files --> convert to jpeg before creating thumbnail\n                // let blob = await fileToBlob(file);\n                // file = await heic2any({\n                //     blob,\n                //     toType: \"image/jpeg\",\n                //     quality: quality\n                // });\n            }\n\n            let imageUrl = URL.createObjectURL(file);\n\n            let image = new Image();\n            image.src = imageUrl;\n\n            await waitUntilImageIsLoaded(imageUrl);\n\n            let imageWidth = image.width;\n            let imageHeight = image.height;\n            let canvas = document.createElement('canvas');\n\n            // resize the canvas and draw the image data into it\n            if (width && height) {\n                canvas.width = width;\n                canvas.height = height;\n            }\n            else if (width) {\n                canvas.width = width;\n                canvas.height = Math.floor(imageHeight * width / imageWidth)\n            }\n            else if (height) {\n                canvas.width = Math.floor(imageWidth * height / imageHeight);\n                canvas.height = height;\n            }\n            else {\n                canvas.width = imageWidth;\n                canvas.height = imageHeight\n            }\n\n            let ctx = canvas.getContext(\"2d\");\n            ctx.drawImage(image, 0, 0, canvas.width, canvas.height);\n\n            let dataUrl = canvas.toDataURL(\"image/jpeg\", quality);\n            resolve(dataUrl);\n        } catch (e) {\n            console.error(e);\n            reject(new Error(`Could not create an image thumbnail from type ${file.type}`));\n        }\n    })\n}\n\n// Resolves returned promise when image is loaded and throws error if image cannot be shown\nfunction waitUntilImageIsLoaded(imageUrl, timeout = 10000) {\n    return new Promise((resolve, reject) => {\n        let image = new Image();\n        image.src = imageUrl;\n\n        const onLoad = () => {\n            cleanup();\n            resolve();\n        };\n\n        const onError = () => {\n            cleanup();\n            reject(new Error('Image failed to load.'));\n        };\n\n        const cleanup = () => {\n            clearTimeout(timeoutId);\n            image.onload = null;\n            image.onerror = null;\n            URL.revokeObjectURL(imageUrl);\n        };\n\n        const timeoutId = setTimeout(() => {\n            cleanup();\n            reject(new Error('Image loading timed out.'));\n        }, timeout);\n\n        image.onload = onLoad;\n        image.onerror = onError;\n    });\n}\n\nasync function decodeBase64Files(base64) {\n    if (!base64) throw new Error('Base64 is empty');\n\n    let bstr = atob(base64), n = bstr.length, u8arr = new Uint8Array(n);\n    while (n--) {\n        u8arr[n] = bstr.charCodeAt(n);\n    }\n\n    const zipBlob = new File([u8arr], 'archive.zip');\n\n    let files = [];\n    const zipEntries = await zipper.getEntries(zipBlob);\n    for (let i = 0; i < zipEntries.length; i++) {\n        let fileBlob = await zipper.getData(zipEntries[i]);\n        files.push(new File([fileBlob], zipEntries[i].filename));\n    }\n    return files\n}\n\nasync function decodeBase64Text(base64) {\n    if (!base64) throw new Error('Base64 is empty');\n\n    return decodeURIComponent(escape(window.atob(base64)))\n}\n\nfunction isUrlValid(url) {\n    try {\n        new URL(url);\n        return true;\n    }\n    catch (e) {\n        return false;\n    }\n}"
  },
  {
    "path": "public/scripts/worker/canvas-worker.js",
    "content": "self.onmessage = (e) => {\n    switch (e.data.type) {\n        case \"createCanvas\": createCanvas(e.data);\n            break;\n        case \"initCanvas\": initCanvas(e.data.footerOffsetHeight, e.data.clientWidth, e.data.clientHeight);\n            break;\n        case \"startAnimation\": startAnimation();\n            break;\n        case \"onShareModeChange\": onShareModeChange(e.data.active);\n            break;\n        case \"switchAnimation\": switchAnimation(e.data.animate);\n            break;\n    }\n};\n\nlet baseColorNormal;\nlet baseColorShareMode;\nlet baseOpacityNormal;\nlet baseOpacityShareMode;\nlet speed;\nlet fps;\n\nlet c;\nlet cCtx;\n\nlet x0, y0, w, h, dw, offset;\n\nlet startTime;\nlet animate = true;\nlet currentFrame = 0;\nlet lastFrame;\nlet baseColor;\nlet baseOpacity;\n\nfunction createCanvas(data) {\n    baseColorNormal = data.baseColorNormal;\n    baseColorShareMode = data.baseColorShareMode;\n    baseOpacityNormal = data.baseOpacityNormal;\n    baseOpacityShareMode = data.baseOpacityShareMode;\n    speed = data.speed;\n    fps = data.fps;\n\n    c = data.canvas;\n    cCtx = c.getContext(\"2d\");\n\n    lastFrame = fps / speed - 1;\n    baseColor = baseColorNormal;\n    baseOpacity = baseOpacityNormal;\n}\n\nfunction initCanvas(footerOffsetHeight, clientWidth, clientHeight) {\n    let oldW = w;\n    let oldH = h;\n    let oldOffset = offset;\n    w = clientWidth;\n    h = clientHeight;\n    offset = footerOffsetHeight - 28;\n\n    if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed\n\n    c.width = w;\n    c.height = h;\n    x0 = w / 2;\n    y0 = h - offset;\n    dw = Math.round(Math.min(Math.max(0.6 * w, h)) / 10);\n\n    drawFrame(currentFrame);\n}\n\nfunction startAnimation() {\n    startTime = Date.now();\n    animateBg();\n}\n\nfunction switchAnimation(state) {\n    if (!animate && state) {\n        // animation starts again. Set startTime to specific value to prevent frame jump\n        startTime = Date.now() - 1000 * currentFrame / fps;\n    }\n    animate = state;\n    requestAnimationFrame(animateBg);\n}\n\nfunction onShareModeChange(active) {\n    baseColor = active ? baseColorShareMode : baseColorNormal;\n    baseOpacity = active ? baseOpacityShareMode : baseOpacityNormal;\n    drawFrame(currentFrame);\n}\n\nfunction drawCircle(ctx, radius) {\n    ctx.lineWidth = 2;\n\n    let opacity = Math.max(0, baseOpacity * (1 - 1.2 * radius / Math.max(w, h)));\n    if (radius > dw * 7) {\n        opacity *= (8 * dw - radius) / dw\n    }\n\n    if (ctx.setStrokeColor) {\n        // older blink/webkit based browsers do not understand opacity in strokeStyle. Use deprecated setStrokeColor instead\n        // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle#webkitblink-specific_note\n        ctx.setStrokeColor(\"grey\", opacity);\n    }\n    else {\n        ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`;\n    }\n    ctx.beginPath();\n    ctx.arc(x0, y0, radius, 0, 2 * Math.PI);\n    ctx.stroke();\n}\n\nfunction drawCircles(ctx, frame) {\n    ctx.clearRect(0, 0, w, h);\n    for (let i = 7; i >= 0; i--) {\n        drawCircle(ctx, dw * i + speed * dw * frame / fps + 33);\n    }\n}\n\nfunction drawFrame(frame) {\n    cCtx.clearRect(0, 0, w, h);\n    drawCircles(cCtx, frame);\n}\n\nfunction animateBg() {\n    let now = Date.now();\n\n    if (!animate && currentFrame === lastFrame) {\n        // Animation stopped and cycle finished -> stop drawing frames\n        return;\n    }\n\n    let timeSinceLastFullCycle = (now - startTime) % (1000 / speed);\n    let nextFrame = Math.trunc(fps * timeSinceLastFullCycle / 1000);\n\n    // Only draw frame if it differs from current frame\n    if (nextFrame !== currentFrame) {\n        drawFrame(nextFrame);\n        currentFrame = nextFrame;\n    }\n\n    requestAnimationFrame(animateBg);\n}"
  },
  {
    "path": "public/service-worker.js",
    "content": "const cacheVersion = 'v1.11.2';\nconst cacheTitle = `pairdrop-cache-${cacheVersion}`;\nconst relativePathsToCache = [\n    './',\n    'index.html',\n    'manifest.json',\n    'styles/styles-main.css',\n    'styles/styles-deferred.css',\n    'scripts/browser-tabs-connector.js',\n    'scripts/localization.js',\n    'scripts/main.js',\n    'scripts/network.js',\n    'scripts/persistent-storage.js',\n    'scripts/ui.js',\n    'scripts/ui-main.js',\n    'scripts/util.js',\n    'scripts/worker/canvas-worker.js',\n    'scripts/libs/heic2any.min.js',\n    'scripts/libs/no-sleep.min.js',\n    'scripts/libs/qr-code.min.js',\n    'scripts/libs/zip.min.js',\n    'sounds/blop.mp3',\n    'sounds/blop.ogg',\n    'images/favicon-96x96.png',\n    'images/favicon-96x96-notification.png',\n    'images/android-chrome-192x192.png',\n    'images/android-chrome-192x192-maskable.png',\n    'images/android-chrome-512x512.png',\n    'images/android-chrome-512x512-maskable.png',\n    'images/apple-touch-icon.png',\n    'fonts/OpenSans/static/OpenSans-Medium.ttf',\n    'lang/ar.json',\n    'lang/be.json',\n    'lang/bg.json',\n    'lang/ca.json',\n    'lang/cs.json',\n    'lang/da.json',\n    'lang/de.json',\n    'lang/en.json',\n    'lang/es.json',\n    'lang/et.json',\n    'lang/eu.json',\n    'lang/fa.json',\n    'lang/fr.json',\n    'lang/he.json',\n    'lang/hu.json',\n    'lang/id.json',\n    'lang/it.json',\n    'lang/ja.json',\n    'lang/kn.json',\n    'lang/ko.json',\n    'lang/nb.json',\n    'lang/nl.json',\n    'lang/nn.json',\n    'lang/pl.json',\n    'lang/pt-BR.json',\n    'lang/ro.json',\n    'lang/ru.json',\n    'lang/sk.json',\n    'lang/ta.json',\n    'lang/tr.json',\n    'lang/uk.json',\n    'lang/zh-CN.json',\n    'lang/zh-HK.json',\n    'lang/zh-TW.json'\n];\nconst relativePathsNotToCache = [\n    'config'\n]\n\nself.addEventListener('install', function(event) {\n    // Perform install steps\n    console.log(\"Cache files for sw:\", cacheVersion);\n    event.waitUntil(\n        caches.open(cacheTitle)\n            .then(function(cache) {\n                return cache\n                    .addAll(relativePathsToCache)\n                    .then(_ => {\n                        console.log('All files cached for sw:', cacheVersion);\n                        self.skipWaiting();\n                    });\n            })\n    );\n});\n\n// fetch the resource from the network\nconst fromNetwork = (request, timeout) =>\n    new Promise((resolve, reject) => {\n        const timeoutId = setTimeout(reject, timeout);\n        fetch(request, {cache: \"no-store\"})\n            .then(response => {\n                if (response.redirected) {\n                    throw new Error(\"Fetch is redirect. Abort usage and cache!\");\n                }\n\n                clearTimeout(timeoutId);\n                resolve(response);\n\n                // Prevent requests that are in relativePathsNotToCache from being cached\n                if (doNotCacheRequest(request)) return;\n\n                updateCache(request)\n                    .then(() => console.log(\"Cache successfully updated for\", request.url))\n                    .catch(err => console.log(\"Cache could not be updated for\", request.url, err));\n            })\n            .catch(error => {\n                // Handle any errors that occurred during the fetch\n                console.error(`Could not fetch ${request.url}.`);\n                reject(error);\n            });\n    });\n\n// fetch the resource from the browser cache\nconst fromCache = request =>\n    caches\n        .open(cacheTitle)\n        .then(cache =>\n            cache.match(request)\n        );\n\nconst rootUrl = location.href.substring(0, location.href.length - \"service-worker.js\".length);\nconst rootUrlLength = rootUrl.length;\n\nconst doNotCacheRequest = request => {\n    const requestRelativePath = request.url.substring(rootUrlLength);\n    return relativePathsNotToCache.indexOf(requestRelativePath) !== -1\n};\n\n// cache the current page to make it available for offline\nconst updateCache = request => new Promise((resolve, reject) => {\n    caches\n        .open(cacheTitle)\n        .then(cache =>\n            fetch(request, {cache: \"no-store\"})\n                .then(response => {\n                    if (response.redirected) {\n                        throw new Error(\"Fetch is redirect. Abort usage and cache!\");\n                    }\n\n                    cache\n                        .put(request, response)\n                        .then(() => resolve());\n                })\n                .catch(reason => reject(reason))\n        );\n});\n\n// general strategy when making a request:\n// 1. Try to retrieve file from cache\n// 2. If cache is not available: Fetch from network and update cache.\n// This way, cached files are only updated if the cacheVersion is changed\nself.addEventListener('fetch', function(event) {\n    const swOrigin = new URL(self.location.href).origin;\n    const requestOrigin = new URL(event.request.url).origin;\n\n    if (swOrigin !== requestOrigin) {\n        // Do not handle requests from other origin\n        event.respondWith(fetch(event.request));\n    }\n    else if (event.request.method === \"POST\") {\n        // Requests related to Web Share Target.\n        event.respondWith((async () => {\n            const share_url = await evaluateRequestData(event.request);\n            return Response.redirect(encodeURI(share_url), 302);\n        })());\n    }\n    else {\n        // Regular requests not related to Web Share Target:\n        // If request is excluded from cache -> respondWith fromNetwork\n        // else -> try fromCache first\n        event.respondWith(\n            doNotCacheRequest(event.request)\n                ? fromNetwork(event.request, 10000)\n                : fromCache(event.request)\n                    .then(rsp => {\n                        // if fromCache resolves to undefined fetch from network instead\n                        if (!rsp) {\n                            throw new Error(\"No match found.\");\n                        }\n                        return rsp;\n                    })\n                    .catch(error => {\n                        console.error(\"Could not retrieve request from cache:\", event.request.url, error);\n                        return fromNetwork(event.request, 10000);\n                    })\n        );\n    }\n});\n\n\n// on activation, we clean up the previously registered service workers\nself.addEventListener('activate', evt => {\n    console.log(\"Activate sw:\", cacheVersion);\n    evt.waitUntil(clients.claim());\n    return evt.waitUntil(\n        caches\n            .keys()\n            .then(cacheNames => {\n                return Promise.all(\n                    cacheNames.map(cacheName => {\n                        if (cacheName !== cacheTitle) {\n                            console.log(\"Delete cache:\", cacheName);\n                            return caches.delete(cacheName);\n                        }\n                    })\n                );\n            })\n    )\n});\n\nconst evaluateRequestData = function (request) {\n    return new Promise(async (resolve) => {\n        const formData = await request.formData();\n        const title = formData.get(\"title\");\n        const text = formData.get(\"text\");\n        const url = formData.get(\"url\");\n        const files = formData.getAll(\"allfiles\");\n\n        const pairDropUrl = request.url;\n\n        if (files && files.length > 0) {\n            let fileObjects = [];\n            for (let i=0; i<files.length; i++) {\n                fileObjects.push({\n                    name: files[i].name,\n                    buffer: await files[i].arrayBuffer()\n                });\n            }\n\n            const DBOpenRequest = indexedDB.open('pairdrop_store');\n            DBOpenRequest.onsuccess = e => {\n                const db = e.target.result;\n                for (let i = 0; i < fileObjects.length; i++) {\n                    const transaction = db.transaction('share_target_files', 'readwrite');\n                    const objectStore = transaction.objectStore('share_target_files');\n\n                    const objectStoreRequest = objectStore.add(fileObjects[i]);\n                    objectStoreRequest.onsuccess = _ => {\n                        if (i === fileObjects.length - 1) resolve(pairDropUrl + '?share_target=files');\n                    }\n                }\n            }\n            DBOpenRequest.onerror = _ => {\n                resolve(pairDropUrl);\n            }\n        }\n        else {\n            let urlArgument = '?share_target=text';\n\n            if (title) urlArgument += `&title=${title}`;\n            if (text) urlArgument += `&text=${text}`;\n            if (url) urlArgument += `&url=${url}`;\n\n            resolve(pairDropUrl + urlArgument);\n        }\n    });\n}\n"
  },
  {
    "path": "public/styles/styles-deferred.css",
    "content": "/* All styles in this sheet are not needed on page load and deferred */\n\n/* Text Input */\n.textarea {\n    box-sizing: border-box;\n    border: none;\n    outline: none;\n    padding: 16px 24px;\n    border-radius: 12px;\n    font-size: 16px;\n    font-family: inherit;\n    display: block;\n    overflow: auto;\n    resize: none;\n    max-height: 350px;\n    word-break: break-word;\n    word-wrap: anywhere;\n    white-space: pre-wrap;\n}\n\n.textarea:before {\n    opacity: 0.5;\n}\n\n/* Peers */\n\nx-peers:has(> x-peer) {\n    --peers-per-row: 10;\n}\n\n@media screen and (min-height: 505px) and (max-height: 649px) and (max-width: 426px),\nscreen and (min-height: 486px) and (max-height: 631px) and (min-width: 426px) {\n    x-peers:has(> x-peer) {\n        --peers-per-row: 3;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(7)) {\n        --peers-per-row: 4;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(10)) {\n        --peers-per-row: 5;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(13)) {\n        --peers-per-row: 6;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(16)) {\n        --peers-per-row: 7;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(19)) {\n        --peers-per-row: 8;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(22)) {\n        --peers-per-row: 9;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(25)) {\n        --peers-per-row: 10;\n    }\n}\n\n@media screen and (min-height: 649px) and (max-width: 425px),\nscreen and (min-height: 631px) and (min-width: 426px) {\n    x-peers:has(> x-peer) {\n        --peers-per-row: 3;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(10)) {\n        --peers-per-row: 4;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(13)) {\n        --peers-per-row: 5;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(16)) {\n        --peers-per-row: 6;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(19)) {\n        --peers-per-row: 7;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(22)) {\n        --peers-per-row: 8;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(25)) {\n        --peers-per-row: 9;\n    }\n\n    x-peers:has(> x-peer:nth-of-type(28)) {\n        --peers-per-row: 10;\n    }\n}\n\n/* Peer */\n\nx-peer {\n    padding: 8px;\n    align-content: start;\n    flex-wrap: wrap;\n}\n\nx-peer input[type=\"file\"] {\n    visibility: hidden;\n    position: absolute;\n}\n\nx-peer label {\n    width: var(--peer-width);\n    touch-action: manipulation;\n    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n    position: relative;\n}\n\nx-peer x-icon {\n    --icon-size: 40px;\n    margin-bottom: 4px;\n    transition: transform 150ms;\n    will-change: transform;\n    display: flex;\n    flex-direction: column;\n}\n\nx-peer .icon-wrapper {\n    width: var(--icon-size);\n    padding: 12px;\n    border-radius: 50%;\n    background: var(--accent-color);\n    background-image: linear-gradient(45deg, var(--accent-color) 40%, color-mix(in srgb, var(--accent-color) 70%, white) 100%);\n    color: white;\n    display: flex;\n}\n\nx-peer.type-secret .icon-wrapper {\n    --accent-color: var(--paired-device-color);\n}\n\nx-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {\n    --accent-color: var(--public-room-color);\n}\n\n.highlight-wrapper {\n    align-self: center;\n    align-items: center;\n    margin: 7px auto 0;\n    height: 6px;\n}\n\n.highlight {\n    width: 15px;\n    height: 6px;\n    border-radius: 4px;\n    margin-left: 1px;\n    margin-right: 1px;\n    --highlight-color: var(--badge-color);\n    background-color: var(--highlight-color);\n    background-image: linear-gradient(180deg, var(--highlight-color) 0%, color-mix(in srgb, var(--highlight-color) 90%, black));\n}\n\n.highlight-room-ip {\n    --highlight-color: var(--primary-color);\n}\n\n.highlight-room-secret {\n    --highlight-color: var(--paired-device-color);\n}\n\n.highlight-room-public-id {\n    --highlight-color: var(--public-room-color);\n}\n\nx-peer:not(.type-ip) .highlight-room-ip {\n    display: none;\n}\n\nx-peer:not(.type-secret) .highlight-room-secret {\n    display: none;\n}\n\nx-peer:not(.type-public-id) .highlight-room-public-id {\n    display: none;\n}\n\nx-peer:not([status]):hover x-icon,\nx-peer:not([status]):focus x-icon {\n    transform: scale(1.05);\n}\n\nx-peer[status] x-icon {\n    box-shadow: none;\n    opacity: 0.8;\n    transform: scale(1);\n}\n\nx-peer.ws-peer {\n    margin-top: -1.5px;\n}\n\nx-peer.ws-peer .progress {\n    margin-top: 3px;\n}\n\nx-peer.ws-peer .icon-wrapper{\n    border: solid 3px var(--ws-peer-color);\n}\n\nx-peer.ws-peer .highlight-wrapper {\n    margin-top: 3px;\n}\n\n#websocket-fallback {\n    opacity: 0.5;\n}\n\n#websocket-fallback > span:nth-of-type(2) {\n    border-bottom: solid 2px var(--ws-peer-color);\n}\n\n.device-descriptor {\n    width: 100%;\n    text-align: center;\n}\n\n.device-descriptor > div {\n    width: 100%;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    text-align: center;\n}\n\n.status,\n.device-name {\n    opacity: 0.7;\n    white-space: nowrap;\n}\n\nx-peer:not([status]) .status,\nx-peer[status] .device-name {\n    display: none;\n}\n\nx-peer[status] {\n    pointer-events: none;\n}\n\nx-peer x-icon {\n    animation: pop 600ms ease-out 1;\n}\n\n@keyframes pop {\n    0% {\n        transform: scale(0.7);\n    }\n\n    40% {\n        transform: scale(1.2);\n    }\n}\n\nx-peer[drop] x-icon {\n    transform: scale(1.1);\n}\n\n/* Checkboxes as slider */\n\n.switch {\n    display: inline-block;\n    height: 20px;\n    position: relative;\n    width: 30px;\n}\n\n.switch input {\n    display:none;\n}\n\n.slider {\n    background-color: rgba(128, 128, 128, 0.5);\n    cursor: pointer;\n    top: 0;\n    bottom: 0;\n    right: 0;\n    left: 0;\n    position: absolute;\n    transition: .4s;\n}\n\n.slider:before {\n    background-color: #fff;\n    content: \"\";\n    position: absolute;\n    top: 2px;\n    left: 2px;\n    width: 16px;\n    height: 16px;\n    transition: .4s;\n}\n\ninput:checked + .slider {\n    background-color: var(--primary-color);\n}\n\ninput:checked + .slider:before {\n    transform: translateX(10px);\n}\n\n.slider.round {\n    border-radius: 20px;\n}\n\n.slider.round:before {\n    border-radius: 50%;\n}\n\n/* Dialog */\n\nx-dialog x-background {\n    background: rgba(0, 0, 0, 0.8);\n    z-index: 30;\n    transition: opacity 300ms;\n    will-change: opacity;\n    padding: 10px 5px 20px;\n    overflow: scroll\n}\n\nx-dialog x-paper {\n    position: relative;\n    display: flex;\n    margin: auto;\n    flex-direction: column;\n    width: 100%;\n    max-width: 450px;\n    z-index: 3;\n    border-radius: 30px;\n    overflow: hidden;\n    box-sizing: border-box;\n    transition: transform 300ms;\n    will-change: transform;\n}\n\nx-paper > .row:first-of-type {\n    background-color: var(--accent-color);\n    margin-bottom: 5px;\n}\n\nx-paper .dialog-title {\n    color: white;\n}\n\n#pair-device-dialog,\n#edit-paired-devices-dialog {\n    --accent-color: var(--paired-device-color);\n}\n\n#public-room-dialog {\n    --accent-color: var(--public-room-color);\n}\n\n#pair-device-dialog ::-moz-selection,\n#pair-device-dialog ::selection {\n    color: black;\n    background: var(--paired-device-color);\n}\n\n#public-room-dialog ::-moz-selection,\n#public-room-dialog ::selection {\n    color: black;\n    background: var(--public-room-color);\n}\n\nx-dialog:not([show]) {\n    pointer-events: none;\n}\n\nx-dialog:not([show]) x-paper {\n    transform: scale(0.1);\n}\n\n/* Pair Devices Dialog & Public Room Dialog */\n\n.input-key-container {\n    width: 100%;\n    display: flex;\n    justify-content: center;\n}\n\n.input-key-container > input {\n    width: 45px;\n    height: 45px;\n    font-size: 30px;\n    padding: 0;\n    text-align: center;\n    text-transform: uppercase;\n    display: -webkit-box !important;\n    display: -webkit-flex !important;\n    display: -moz-flex !important;\n    display: -ms-flexbox !important;\n    display: flex !important;\n    -webkit-justify-content: center;\n    -ms-justify-content: center;\n    justify-content: center;\n}\n\n.input-key-container > input {\n    margin: 0 3px;\n}\n\n.input-key-container.six-chars > input:nth-of-type(4) {\n    margin-left: 5%;\n}\n\n.key {\n    -webkit-user-select: text;\n    -moz-user-select: text;\n    user-select: text;\n    display: inline-block;\n    font-size: 45px;\n    letter-spacing: 18px;\n    text-indent: 15px;\n    margin: 10px 0;\n}\n\n.key-qr-code {\n    width: fit-content;\n    align-self: center;\n    margin-top: 5px;\n    margin-bottom: 10px;\n}\n\n.key-instructions {\n    flex-direction: column;\n    margin: 0;\n}\n\nx-dialog h2 {\n    margin-top: 5px;\n    margin-bottom: 0;\n}\n\nx-dialog hr {\n    height: 1px;\n    border: none;\n    width: 100%;\n    background-color: var(--border-color);\n}\n\n.hr-note {\n    margin-top: 13px;\n    margin-bottom: 21px;\n}\n\n.hr-note hr {\n    margin-bottom: -1px;\n}\n\n.hr-note > div {\n    height: 0;\n    transform: translateY(-10px);\n}\n\n\n.hr-note > div > span {\n    padding: 3px 10px;\n    border-radius: 20px;\n    color: rgb(var(--text-color));\n    background-color: var(--dialog-bg-color);\n    border: var(--border-color) solid 3px;\n    text-transform: uppercase;\n}\n\n/* Edit Paired Devices Dialog */\n.paired-devices-wrapper:empty:before {\n    content: attr(data-empty);\n}\n\n.paired-devices-wrapper:empty {\n    padding: 10px;\n}\n\n.paired-devices-wrapper {\n    margin-top: -5px;\n    border-bottom: solid 4px var(--paired-device-color);\n    max-height: 65vh;\n    overflow: scroll;\n}\n\n.paired-device {\n    display: flex;\n    justify-content: space-between;\n    flex-direction: column;\n    align-items: center;\n}\n\n.paired-device:empty {\n    padding: 47px;\n}\n\n.paired-device:not(:last-child) {\n    border-bottom: solid 4px var(--paired-device-color);\n}\n\n.paired-device > .display-name,\n.paired-device > .device-name {\n    width: 100%;\n    height: 36px;\n    display: flex;\n    align-items: center;\n    text-align: center;\n    align-self: center;\n    border-bottom: solid 2px rgba(128, 128, 128, 0.5);\n    opacity: 1;\n}\n\n.paired-device > .button-wrapper > * {\n    min-height: 38px;\n    padding-left: 5px;\n    padding-right: 5px;\n}\n\n.paired-device > .button-wrapper > :not(:last-child) {\n    border-right: solid 1px rgba(128, 128, 128, 0.5);\n}\n\n.paired-device > .button-wrapper > :not(:first-child) {\n    border-left: solid 1px rgba(128, 128, 128, 0.5);\n}\n\n.paired-device * {\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n/* button row*/\n.btn-row .btn {\n    margin: 3px;\n    flex-grow: 1;\n    height: 50px;\n    width: 120px; /* fixed width needed to ensure same width for all buttons */\n}\n\nx-paper > .btn-row {\n    margin: 5px 10px 10px;\n}\n\n.language-buttons > .btn {\n    border-top: solid var(--lang-hr-color) 2px;\n    padding: 7px;\n    font-size: 12px;\n}\n\n.language-buttons > .btn:last-of-type {\n    border-bottom: solid var(--lang-hr-color) 2px;\n}\n\n/* Ensure click event target is always button and never span */\n.language-buttons span {\n    z-index: -1;\n}\n\n.language-buttons > .current:after {\n    position: absolute;\n    right: 20px;\n    content: \"✓\";\n    color: var(--primary-color);\n    font-size: 20px;\n}\n\n.file-description {\n    max-width: 100%;\n}\n\n.file-description span {\n    display: inline;\n    word-break: normal;\n}\n\n.file-name {\n    font-style: italic;\n    max-width: 100%;\n    margin-top: 5px;\n}\n\n.file-stem {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    padding-right: 1px;\n}\n\n/* Send Text Dialog */\nx-dialog .dialog-subheader {\n    padding-top: 16px;\n    padding-bottom: 16px;\n}\n\n.display-name-wrapper {\n    padding-bottom: 0;\n}\n\n#send-text-dialog,\n#receive-text-dialog {\n    font-size: 16px; /* prevents auto-zoom on edit */\n}\n\n.textarea.overflowing {\n    --shadow-color-rgb: var(--shadow-color-secondary-rgb);\n    --shadow-color-cover-rgb: var(--shadow-color-secondary-cover-rgb);\n}\n\n#edit-paired-devices-dialog {\n    --shadow-color-rgb: var(--shadow-color-dialog-rgb);\n    --shadow-color-cover-rgb: var(--shadow-color-dialog-cover-rgb);\n}\n\n/* Receive Text Dialog */\n\n#receive-text-dialog #text {\n    word-break: break-all;\n    max-height: 400px;\n    padding: 10px;\n    overflow-x: hidden;\n    overflow-y: scroll;\n    -webkit-user-select: text;\n    -moz-user-select: text;\n    user-select: text;\n}\n\n#receive-text-dialog #text a:hover {\n    text-decoration: underline;\n}\n\n#receive-text-dialog h3 {\n    /* Select the received text when double-clicking the dialog */\n    user-select: none;\n    pointer-events: none;\n}\n\n/* Do not call it 'share-panel', 'share-pannel' or 'sharepanel' as iOS Safari does not show any element with these classnames... */\n\n.shr-panel {\n    min-width: 250px;\n    max-width: calc(100vw - 20px);\n    overflow: hidden;\n    color: white;\n    background-color: var(--primary-color);\n    background-image: linear-gradient(225deg, var(--accent-color) 0%, color-mix(in srgb, var(--accent-color) 60%, black) 100%);\n}\n\n.shr-panel > div {\n    margin: 4px 2px;\n}\n\n.shr-panel > div:not(:first-child) {\n    margin-top: 2px;\n}\n\n.shr-panel .thumb > div {\n    width: 36px;\n    height: 36px;\n    background: white;\n    border-radius: 6px;\n    margin-right: 6px;\n}\n\n.shr-panel .thumb > .text-thumb > svg {\n    width: 18px;\n    height: 36px;\n}\n\n.shr-panel .thumb > .file-thumb > svg {\n    width: 36px;\n    height: 36px;\n}\n\n.shr-panel .thumb > .image-thumb {\n    background-size: cover;\n    background-position: center;\n}\n\n.shr-panel .btn {\n    height: 36px;\n}\n\n.share-descriptor {\n    justify-content: center;\n}\n\n.share-descriptor > span {\n    display: inline;\n    margin-bottom: 0;\n    margin-top: 0;\n    height: 20px;\n    max-width: 15rem;\n    text-overflow: ellipsis;\n    overflow: hidden;\n    white-space: nowrap;\n}\n\n.share-descriptor > span:first-child {\n    font-weight: bold;\n}\n\n.share-descriptor > span:not(:first-child) {\n    font-size: small;\n}\n\n#base64-paste-btn,\n#base64-paste-dialog .textarea {\n    width: 100%;\n    height: 40vh;\n    border: solid 12px #438cff;\n    margin: 6px;\n}\n\n#base64-paste-dialog .textarea {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    text-align: center;\n}\n\n#base64-paste-dialog .textarea::before {\n    font-size: 14px;\n    letter-spacing: 0.12em;\n    color: var(--primary-color);\n    font-weight: 700;\n    text-transform: uppercase;\n    white-space: pre-wrap;\n}\n\n/* Peer loading Indicator */\n\n.progress {\n    width: 80px;\n    height: 80px;\n    position: absolute;\n    top: -8px;\n    clip: rect(0px, 80px, 80px, 40px);\n    --progress: rotate(0deg);\n    transition: transform 200ms;\n}\n\n.circle {\n    width: 72px;\n    height: 72px;\n    border: 4px solid var(--primary-color);\n    border-radius: 40px;\n    position: absolute;\n    clip: rect(0px, 40px, 80px, 0px);\n    will-change: transform;\n    transform: var(--progress);\n}\n\n.over50 {\n    clip: rect(auto, auto, auto, auto);\n}\n\n.over50 .circle.right {\n    transform: rotate(180deg);\n}\n\n/*\n    Color Themes\n*/\n\n/* Colored Elements */\n\nx-dialog x-paper {\n    background-color: var(--dialog-bg-color);\n}\n\n.textarea {\n    color: rgb(var(--text-color)) !important;\n    background-color: var(--bg-color-secondary) !important;\n}\n\n.textarea *:not(a) {\n    margin: 0 !important;\n    padding: 0 !important;\n    color: unset !important;\n    background: unset !important;\n    border: unset !important;\n    opacity: unset !important;\n    font-family: inherit !important;\n    font-size: inherit !important;\n    font-style: unset !important;\n    font-weight: unset !important;\n}\n\nx-dialog a {\n    color: var(--primary-color);\n}\n\n/* Image/Video/Audio Preview */\n.file-preview {\n    margin-bottom: 15px;\n}\n\n.file-preview:empty {\n    display: none;\n}\n\n.file-preview > img,\n.file-preview > audio,\n.file-preview > video {\n    max-width: 100%;\n    max-height: 40vh;\n    margin: auto;\n    display: block;\n}"
  },
  {
    "path": "public/styles/styles-main.css",
    "content": "/* All styles in this sheet are needed on page load */\n\n/* Layout */\n\nhtml,\nbody {\n    margin: 0;\n    display: flex;\n    flex-direction: column;\n    width: 100vw;\n    overflow-x: hidden;\n    overscroll-behavior: none;\n    overflow-y: hidden;\n    transition: color 300ms;\n}\n\nbody {\n    height: 100%;\n}\n\nhtml {\n    height: 100%;\n}\n\n.fw {\n    width: 100%;\n}\n\n.p-1 {\n    padding: 5px;\n}\n\n.p-2 {\n    padding: 10px;\n}\n\n.pb-0 {\n    padding-bottom: 0;\n}\n\n.mx-1 {\n    margin-left: 5px;\n    margin-right: 5px;\n}\n\n.m-1 {\n    margin: 5px;\n}\n\n.cursive {\n    font-style: italic;\n}\n\n.wrap {\n    display: flex;\n    flex-wrap: wrap;\n}\n\n.wrap-reverse {\n    display: flex;\n    flex-wrap: wrap-reverse;\n}\n\n.grow {\n    display: flex;\n    flex-grow: 1;\n}\n\n.grow-2 {\n    display: flex;\n    flex-grow: 2;\n}\n\n.grow-5 {\n    display: flex;\n    flex-grow: 5;\n}\n\n.shrink {\n    display: flex;\n    flex-shrink: 1;\n}\n\n.flex {\n    display: flex;\n}\n\n.align-center {\n    align-items: center;\n}\n\n.space-evenly {\n    justify-content: space-evenly;\n}\n\n.space-between {\n    justify-content: space-between;\n}\n\n.row {\n    display: flex;\n    flex-direction: row;\n}\n\n.row-reverse {\n    display: flex;\n    flex-direction: row-reverse;\n}\n\n.column {\n    display: flex;\n    flex-direction: column;\n}\n\n.center {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.full {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n}\n\n.pointer {\n    cursor: pointer;\n}\n\nheader {\n    position: relative;\n    align-items: baseline;\n    padding: 8px 12px;\n    box-sizing: border-box;\n    width: 100vw;\n    z-index: 20;\n    top: 0;\n    right: 0;\n    min-height: 56px;\n}\n\nheader.overflow-hidden {\n    overflow: hidden;\n}\n\nheader:not(.overflow-expanded) {\n    height: 56px;\n}\n\nheader > * {\n    margin-left: 4px;\n    margin-right: 4px;\n}\n\nheader > * {\n    display: flex;\n    flex-direction: column;\n    align-self: flex-start;\n    touch-action: manipulation;\n}\n\nheader > .icon-button {\n    height: 40px;\n}\n\nheader * {\n    transition: all 300ms;\n}\n\n#theme-wrapper > div {\n    display: flex;\n    flex-direction: column;\n}\n\n/* expand theme buttons */\n#theme-wrapper:not(:hover) .icon-button:not(.selected) {\n    height: 0;\n    opacity: 0;\n}\n\n#theme-wrapper:hover::before {\n    border-radius: 20px;\n    background: var(--primary-color);\n    opacity: 0.2;\n    transition: opacity 300ms;\n    content: '';\n    position: absolute;\n    width: 40px;\n    height: 120px;\n    top: 0;\n    margin-top: 8px;\n    margin-bottom: 8px;\n}\n\n@media (hover: hover) and (pointer: fine) {\n    #theme-wrapper:hover .icon-button:not(.selected):hover:before {\n        opacity: 0.3;\n    }\n\n    #theme-wrapper:hover .icon-button.selected::before {\n        opacity: 0.3;\n    }\n}\n\n@media (hover: none) and (pointer: coarse) {\n    #theme-wrapper:before {\n        opacity: 0.3 !important;\n        height: 40px !important;\n    }\n\n    #theme-wrapper .icon-button:before {\n        opacity: 0;\n    }\n\n    #theme-wrapper .icon-button:not(.selected) {\n        height: 0;\n        opacity: 0;\n        pointer-events: none;\n    }\n\n    #theme-wrapper > div {\n        flex-direction: column-reverse;\n    }\n}\n\n#expand > .icon {\n    transition: transform 150ms ease-out\n}\n\nhtml:not([dir=\"rtl\"]) #expand.flipped  > .icon {\n    transform: rotate(-90deg);\n}\n\nhtml[dir=\"rtl\"] #expand.flipped  > .icon {\n    transform: rotate(90deg);\n}\n\n[hidden] {\n    display: none !important;\n}\n\n\n/* Typography */\n\n@font-face {\n    font-family: \"Open Sans\";\n    src: url('../fonts/OpenSans/static/OpenSans-Medium.ttf') format('truetype');\n    font-display: swap;\n}\n\nbody {\n    font-family: \"Open Sans\", -apple-system, BlinkMacSystemFont, sans-serif;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    text-rendering: optimizeLegibility;\n    font-variant-ligatures: common-ligatures;\n    font-kerning: normal;\n}\n\nh1 {\n    font-size: 34px;\n    font-weight: 400;\n    letter-spacing: -.01em;\n    line-height: 40px;\n    margin: 0 0 4px;\n}\n\nh2 {\n    font-size: 22px;\n    font-weight: 400;\n    letter-spacing: -.012em;\n    line-height: 32px;\n    color: var(--primary-color);}\n\nh3 {\n    font-size: 20px;\n    font-weight: 500;\n    margin: 16px 0;\n    color: var(--primary-color);\n}\n\n.font-subheading {\n    font-size: 14px;\n    font-weight: 400;\n    line-height: 18px;\n    word-break: normal;\n}\n\n.text-center {\n    text-align: center;\n}\n\n.text-white {\n    color: white !important;\n}\n\n.font-body1,\nbody {\n    font-size: 14px;\n    font-weight: 400;\n    line-height: 20px;\n}\n\n.font-body2 {\n    font-size: 12px;\n    line-height: 18px;\n}\n\na,\n.icon-button {\n    text-decoration: none;\n    color: currentColor;\n    cursor: pointer;\n}\n\ninput {\n    cursor: pointer;\n}\n\ninput[type=\"checkbox\"] {\n    min-width: 13px;\n}\n\nx-noscript {\n    background: var(--primary-color);\n    color: white;\n    z-index: 2;\n}\n\n\n/* Icons */\n\n.icon {\n    width: var(--icon-size);\n    height: var(--icon-size);\n    fill: currentColor;\n}\n\n\n\n/* Shadows */\n\n[shadow=\"1\"] {\n    box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14),\n        0 1px 8px 0 rgba(0, 0, 0, 0.12),\n        0 3px 3px -2px rgba(0, 0, 0, 0.4);\n}\n\n[shadow=\"2\"] {\n    box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14),\n        0 1px 10px 0 rgba(0, 0, 0, 0.12),\n        0 2px 4px -1px rgba(0, 0, 0, 0.4);\n}\n\n.overflowing {\n    background:\n        /* Shadow covers */\n            linear-gradient(rgb(var(--shadow-color-cover-rgb)) 30%, rgba(var(--shadow-color-cover-rgb), 0)),\n            linear-gradient(rgba(var(--shadow-color-cover-rgb), 0), rgb(var(--shadow-color-cover-rgb)) 70%) 0 100%,\n                /* Shadows */\n            radial-gradient(farthest-side at 50% 0, rgba(var(--shadow-color-rgb), .2), rgba(var(--shadow-color-rgb), 0)),\n            radial-gradient(farthest-side at 50% 100%, rgba(var(--shadow-color-rgb), .2), rgba(var(--shadow-color-rgb), 0))\n            0 100%;\n    background-repeat: no-repeat;\n    background-size: 100% 60px, 100% 60px, 100% 24px, 100% 24px;\n    background-attachment: local, local, scroll, scroll;\n}\n\n\n/* Animations */\n\n/* Opacity for elements at keyframe 100% is set on element (default 1) */\n@keyframes fade-in {\n    0% {\n        opacity: 0;\n    }\n}\n\n#center {\n    position: relative;\n    display: flex;\n    flex-direction: column-reverse;\n    flex-grow: 1;\n    justify-content: space-around;\n    align-items: center;\n    overflow-x: hidden;\n    overflow-y: scroll;\n    overscroll-behavior-x: none;\n}\n\n\n/* Peers  */\n\nx-peers {\n    position: relative;\n    display: flex;\n    flex-flow: row wrap;\n\n    z-index: 2;\n    transition: background-color 0.5s ease;\n    overflow-y: scroll;\n    overflow-x: hidden;\n    overscroll-behavior-x: none;\n    scrollbar-width: none;\n\n    --peers-per-row: 6; /* default if browser does not support :has selector */\n    --x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));\n    width: var(--x-peers-width);\n}\n\n/* Empty Peers List */\n\nx-no-peers {\n    display: flex;\n    flex-direction: column;\n    padding: 8px;\n    height: 137px;\n    text-align: center;\n}\n\nx-no-peers h2,\nx-no-peers a {\n    color: var(--primary-color);\n    margin-bottom: 5px;\n}\n\nx-peers:not(:empty)+x-no-peers {\n    display: none;\n}\n\nx-no-peers::before {\n    color: var(--primary-color);\n    font-size: 24px;\n    font-weight: 400;\n    letter-spacing: -.012em;\n    line-height: 32px;\n}\n\nx-no-peers[drop-bg]::before {\n    content: attr(data-drop-bg);\n}\n\nx-no-peers[drop-bg] * {\n    display: none;\n}\n\n/* Footer */\n\nfooter {\n    position: relative;\n    z-index: 2;\n    align-items: center;\n    text-align: center;\n    cursor: default;\n    margin: auto 5px 5px;\n}\n\nfooter .logo {\n    --icon-size: 80px;\n    margin-bottom: 8px;\n    color: var(--primary-color);\n    margin-top: -10px;\n}\n\n.border {\n    border: 2px solid var(--border-color);\n}\n\n.panel {\n    font-size: 14px;\n    padding: 2px;\n    background-color: rgb(var(--bg-color));\n    transition: background-color 0.5s ease;\n    min-height: 24px;\n}\n\n.panel.column {\n    border-radius: 16px;\n}\n\n.panel.row {\n    border-radius: 12px;\n}\n\n.panel > div:first-of-type {\n    padding-left: 4px;\n    padding-right: 4px;\n}\n\n/* You can be discovered wrapper */\n.discovery-wrapper {\n    margin: 15px auto auto;\n}\n\n.discovery-wrapper .badge {\n    word-break: keep-all;\n    margin: 2px;\n}\n\n.badge {\n    border-radius: 0.4rem;\n    padding-right: 0.3rem;\n    padding-left: 0.3em;\n    background-color: var(--badge-color);\n    color: white;\n    white-space: nowrap;\n}\n\n.badge-room-ip {\n    --badge-color: var(--primary-color);\n}\n\n.badge-room-secret {\n    --badge-color: var(--paired-device-color);\n}\n\n.badge-room-public-id {\n    --badge-color: var(--public-room-color);\n}\n\n.known-as-wrapper {\n    font-size: 16px; /* prevents auto-zoom on edit */\n}\n\n#display-name {\n    position: relative;\n    display: inline-block;\n    text-align: left;\n    border: none;\n    outline: none;\n    height: 20px;\n    max-width: 15em;\n    text-overflow: ellipsis;\n    overflow: hidden;\n    white-space: nowrap;\n    cursor: text;\n    margin-bottom: -6px;\n    padding-bottom: 0.1rem;\n    border-radius: 1.3rem/30%;\n    border-right: solid 1rem transparent;\n    border-left: solid 1rem transparent;\n    background-clip: padding-box;\n    z-index: 1;\n}\n\n.edit-pen {\n    width: 1rem;\n    height: 1rem;\n    margin-bottom: -2px;\n    position: relative;\n}\n\n#display-name:focus::before {\n    display: none;\n}\n\nhtml:not([dir=\"rtl\"]) #display-name,\nhtml:not([dir=\"rtl\"]) .edit-pen {\n    margin-left: -1rem;\n}\n\nhtml[dir=\"rtl\"] #display-name,\nhtml[dir=\"rtl\"] .edit-pen {\n    margin-right: -1rem;\n}\n\nhtml[dir=\"rtl\"] .edit-pen {\n    transform: rotateY(180deg);\n}\n\n/* Dialogs needed on page load */\nx-dialog:not([show]) x-background {\n    opacity: 0;\n}\n\n/* Button */\n\n.btn {\n    font-family: \"Open Sans\", -apple-system, BlinkMacSystemFont, sans-serif;\n    padding: 2px 16px 0;\n    box-sizing: border-box;\n    font-size: 14px;\n    line-height: 24px;\n    font-weight: 700;\n    letter-spacing: 0.12em;\n    text-transform: uppercase;\n    cursor: pointer;\n    user-select: none;\n    background: inherit;\n    color: var(--accent-color);\n    overflow: hidden;\n}\n\n.btn-small {\n    font-size: 12px;\n    line-height: 22px;\n}\n\n.btn[disabled] {\n    color: var(--btn-disabled-color);\n    cursor: not-allowed;\n}\n\n\n.btn,\n.icon-button {\n    position: relative;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n    touch-action: manipulation;\n    border: none;\n    outline: none;\n}\n\n.btn:before,\n.icon-button:before {\n    content: '';\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    opacity: 0;\n    background-color: var(--accent-color);\n    transition: opacity 300ms;\n}\n\n.icon-button:before {\n    z-index: -1;\n}\n\n.btn:not([disabled]):hover:before,\n.icon-button:hover:before {\n    opacity: 0.3;\n}\n\n.btn[selected],\n.icon-button[selected] {\n    opacity: 0.3;\n}\n\n.btn:focus:before,\n.icon-button:focus:before {\n    opacity: 0.4;\n}\n\n.btn-round {\n    border-radius: 50%;\n}\n\n.btn-rounded {\n    border-radius: 12px;\n}\n\n.btn-small.btn-rounded {\n    border-radius: 6px;\n}\n\n.btn-grey {\n    background-color: var(--bg-color-secondary);\n}\n\n.btn-dark {\n    background-color: #262628;\n}\n\n.btn-primary {\n    background: var(--primary-color);\n    color: rgb(var(--bg-color));\n}\n\nbutton::-moz-focus-inner {\n    border: 0;\n}\n\n\n/* Icon Button */\n.icon-button {\n    width: 40px;\n    height: 40px;\n}\n\n.icon-button:before {\n    border-radius: 50%;\n}\n\n\n/* Info Animation */\n#about {\n    color: white;\n    z-index: 32;\n    overflow: hidden;\n    pointer-events: none;\n    text-align: center;\n}\n\n#about header {\n    z-index: 1;\n}\n\n#about:not(:target) header {\n    transition-delay: 400ms;\n}\n\n#about:target header {\n    transition-delay: 100ms;\n}\n\n#about > * {\n    transition: opacity 300ms ease 300ms;\n    will-change: opacity;\n    pointer-events: all;\n}\n\n#about:not(:target) > header,\n#about:not(:target) > section {\n    opacity: 0;\n    pointer-events: none;\n    transition-delay: 0s;\n}\n\n#about .logo {\n    --icon-size: 96px;\n}\n\n#about .title-wrapper {\n    display: flex;\n    align-items: baseline;\n}\n\n#about .title-wrapper > div {\n    margin-left: 0.4em;\n}\n\n#about x-background {\n    position: absolute;\n    --size: max(max(230vw, 230vh), calc(150vh + 150vw));\n    --size-half: calc(var(--size)/2);\n    top: calc(28px - var(--size-half));\n    width: var(--size);\n    height: var(--size);\n    z-index: -1;\n    background: var(--primary-color);\n    background-image: radial-gradient(circle at calc(50% - 36px), var(--primary-color) 0%, black 80%);\n    --crop-size: 0px;\n    clip-path: circle(var(--crop-size));\n    /* For clients < iOS 13.1 */\n    -webkit-clip-path: circle(var(--crop-size));\n}\n\nhtml:not([dir=\"rtl\"]) #about x-background {\n    right: calc(36px - var(--size-half));\n}\n\nhtml[dir=\"rtl\"] #about x-background {\n    left: calc(36px - var(--size-half));\n}\n\n\n/* Hack such that initial scale(0) isn't animated */\n#about x-background {\n    will-change: clip-path;\n    transition: clip-path 800ms cubic-bezier(0.77, 0, 0.175, 1);\n}\n\n#about:target x-background {\n    --crop-size: var(--size);\n}\n\n#about .row a {\n    margin: 8px 8px -16px;\n}\n\n#about section {\n    flex-grow: 1;\n}\n\ncanvas.circles {\n    width: 100vw;\n    position: absolute;\n    z-index: -10;\n    top: 0;\n    left: 0;\n}\n\n/* Generic placeholder */\n[placeholder]:empty:before {\n    content: attr(placeholder);\n}\n\n/* Toast */\n\n.toast-container {\n    padding: 0 8px 24px;\n    overflow: hidden;\n    pointer-events: none;\n}\n\nx-toast {\n    display: flex;\n    justify-content: space-between;\n    position: absolute;\n    min-height: 48px;\n    top: 50px;\n    max-width: 400px;\n    background-color: rgb(var(--text-color));\n    color: var(--dialog-bg-color);\n    align-items: center;\n    box-sizing: border-box;\n    padding: 8px;\n    z-index: 40;\n    transition: opacity 200ms, transform 300ms ease-out;\n    cursor: default;\n    line-height: 24px;\n    border-radius: 12px;\n    pointer-events: all;\n}\n\nx-toast.top-row {\n    top: 3px;\n}\n\nx-toast:not([show]) {\n    opacity: 0;\n    transform: translateY(calc(-100% + -55px));\n}\n\nx-toast span {\n    flex-grow: 1;\n    margin: auto 4px auto 10px\n}\n\nx-dialog[show] ~ div x-toast {\n    background-color: var(--lt-dialog-bg-color);\n    color: rgb(var(--lt-text-color));\n}\n\n/* Instructions */\n\nx-instructions {\n    display: flex;\n    position: relative;\n    opacity: 0.5;\n    text-align: center;\n    margin-right: 10px;\n    margin-left: 10px;\n    min-height: max(6vh, 40px);\n    flex-direction: column;\n    justify-content: end;\n}\n\nx-instructions:not([drop-peer]):not([drop-bg]):before {\n    content: attr(mobile);\n}\n\nx-instructions[drop-peer]:before {\n    content: attr(data-drop-peer);\n}\n\nx-instructions[drop-bg]:not([drop-peer]):before {\n    content: attr(data-drop-bg);\n}\n\n\nx-peers:empty,\nx-peers:empty~x-instructions {\n    display: none;\n}\n\n@media (hover: none) and (pointer: coarse) {\n    x-peer {\n        transform: scale(0.95);\n        padding: 4px;\n    }\n}\n\n/* Prevent Cumulative Layout Shift */\n\n.fade-in {\n    animation: fade-in 600ms;\n    animation-fill-mode: backwards;\n}\n\n.no-animation-on-load {\n    animation-iteration-count: 0;\n}\n\n.opacity-0 {\n    opacity: 0;\n}\n\n/* Responsive Styles */\n\n@media screen and (min-height: 800px) {\n    footer {\n        padding-bottom: 10px;\n    }\n}\n\n@media (hover: hover) and (pointer: fine) {\n    x-instructions:not([drop-peer]):not([drop-bg]):before {\n        content: attr(desktop);\n    }\n}\n\n/* PWA Standalone styles */\n@media all and (display-mode: standalone) {\n    footer {\n        padding-bottom: 34px;\n    }\n}\n\n/* Constants */\n\n:root {\n    --icon-size: 24px;\n    --peer-width: 120px;\n    color-scheme: light dark;\n}\n\n/*\n    Color Themes\n*/\n\n/* Default colors */\nbody {\n    /* Constant colors */\n    --primary-color: #4285f4;\n    --paired-device-color: #00a69c;\n    --public-room-color: #ed9d01;\n    --accent-color: var(--primary-color);\n    --ws-peer-color: #ff6b6b;\n    --btn-disabled-color: #5B5B66;\n\n    /* shadows */\n    --shadow-color-rgb: var(--text-color);\n    --shadow-color-cover-rgb: var(--bg-color);\n\n    /* Light theme colors */\n    --lt-text-color: 51,51,51;\n    --lt-dialog-bg-color: #fff;\n    --lt-bg-color: 255,255,255;\n    --lt-bg-color-secondary: #f2f2f2;\n    --lt-border-color: #757575;\n    --lt-badge-color: #757575;\n    --lt-lang-hr-color: #DDD;\n\n    --lt-shadow-color-secondary-rgb: 0,0,0;\n    --lt-shadow-color-secondary-cover-rgb: 242,242,242;\n    --lt-shadow-color-dialog-rgb: 0,0,0;\n    --lt-shadow-color-dialog-cover-rgb: 242,242,242;\n\n    /* Dark theme colors */\n    --dt-text-color: 238,238,238;\n    --dt-dialog-bg-color: #141414;\n    --dt-bg-color: 0,0,0;\n    --dt-bg-color-secondary: #262628;\n    --dt-border-color: #757575;\n    --dt-badge-color: #757575;\n    --dt-lang-hr-color: #404040;\n\n    --dt-shadow-color-secondary-rgb: 255,255,255;\n    --dt-shadow-color-secondary-cover-rgb: 38,38,38;\n    --dt-shadow-color-dialog-rgb: 255,255,255;\n    --dt-shadow-color-dialog-cover-rgb: 38,38,38;\n}\n\n/* Light theme colors */\nbody {\n    --text-color: var(--lt-text-color);\n    --dialog-bg-color: var(--lt-dialog-bg-color);\n    --bg-color: var(--lt-bg-color);\n    --bg-color-secondary: var(--lt-bg-color-secondary);\n    --border-color: var(--lt-border-color);\n    --badge-color: var(--lt-badge-color);\n    --lang-hr-color: var(--lt-lang-hr-color);\n\n    --shadow-color-secondary-rgb: var(--lt-shadow-color-secondary-rgb);\n    --shadow-color-secondary-cover-rgb: var(--lt-shadow-color-secondary-cover-rgb);\n    --shadow-color-dialog-rgb: var(--lt-shadow-color-dialog-rgb);\n    --shadow-color-dialog-cover-rgb: var(--lt-shadow-color-dialog-cover-rgb);\n}\n\n/* Dark theme colors */\nbody.dark-theme {\n    --text-color: var(--dt-text-color);\n    --dialog-bg-color: var(--dt-dialog-bg-color);\n    --bg-color: var(--dt-bg-color);\n    --bg-color-secondary: var(--dt-bg-color-secondary);\n    --border-color: var(--dt-border-color);\n    --badge-color: var(--dt-badge-color);\n    --lang-hr-color: var(--dt-lang-hr-color);\n\n    --shadow-color-secondary-rgb: var(--dt-shadow-color-secondary-rgb);\n    --shadow-color-secondary-cover-rgb: var(--dt-shadow-color-secondary-cover-rgb);\n    --shadow-color-dialog-rgb: var(--dt-shadow-color-dialog-rgb);\n    --shadow-color-dialog-cover-rgb: var(--dt-shadow-color-dialog-cover-rgb);\n}\n\n/* Styles for users who prefer dark mode at the OS level */\n@media (prefers-color-scheme: dark) {\n\n    /* defaults to dark theme */\n    body {\n        --text-color: var(--dt-text-color);\n        --dialog-bg-color: var(--dt-dialog-bg-color);\n        --bg-color: var(--dt-bg-color);\n        --bg-color-secondary: var(--dt-bg-color-secondary);\n        --border-color: var(--dt-border-color);\n        --badge-color: var(--dt-badge-color);\n        --lang-hr-color: var(--dt-lang-hr-color);\n\n        --shadow-color-secondary-rgb: var(--dt-shadow-color-secondary-rgb);\n        --shadow-color-secondary-cover-rgb: var(--dt-shadow-color-secondary-cover-rgb);\n        --shadow-color-dialog-rgb: var(--dt-shadow-color-dialog-rgb);\n        --shadow-color-dialog-cover-rgb: var(--dt-shadow-color-dialog-cover-rgb);\n    }\n\n\n    /* Override dark mode with light mode styles if the user decides to swap */\n    body.light-theme {\n        --text-color: var(--lt-text-color);\n        --dialog-bg-color: var(--lt-dialog-bg-color);\n        --bg-color: var(--lt-bg-color);\n        --bg-color-secondary: var(--lt-bg-color-secondary);\n        --border-color: var(--lt-border-color);\n        --badge-color: var(--lt-badge-color);\n        --lang-hr-color: var(--lt-lang-hr-color);\n\n        --shadow-color-secondary-rgb: var(--lt-shadow-color-secondary-rgb);\n        --shadow-color-secondary-cover-rgb: var(--lt-shadow-color-secondary-cover-rgb);\n        --shadow-color-dialog-rgb: var(--lt-shadow-color-dialog-rgb);\n        --shadow-color-dialog-cover-rgb: var(--lt-shadow-color-dialog-cover-rgb);\n    }\n}\n\n/* Colored Elements */\nbody {\n    color: rgb(var(--text-color));\n    background-color: rgb(var(--bg-color));\n    transition: background-color 0.5s ease;\n}\n\n/* Gradient for wifi-tether icon */\n#primaryGradient .start-color {\n    stop-color: var(--primary-color);\n}\n\n@supports (stop-color: color-mix(in srgb, blue 50%, black)) {\n    #primaryGradient .start-color {\n        stop-color: color-mix(in srgb, var(--primary-color) 80%, white);\n    }\n}\n\n#primaryGradient .stop-color {\n    stop-color: var(--primary-color);\n}\n\n\n/*\n    Edge specific styles\n*/\n@supports (-ms-ime-align: auto) {\n\n    html,\n    body {\n        overflow: hidden;\n    }\n}\n\n/*\n    Browser specific styles\n*/\n\nbody {\n    /* mobile viewport bug fix */\n    min-height: -moz-available;          /* WebKit-based browsers will ignore this. */\n    min-height: -webkit-fill-available;  /* Mozilla-based browsers will ignore this. */\n    min-height: fill-available;\n}\n\nhtml {\n    min-height: -moz-available;          /* WebKit-based browsers will ignore this. */\n    min-height: -webkit-fill-available;  /* Mozilla-based browsers will ignore this. */\n    min-height: fill-available;\n}\n\n/* webkit scrollbar style*/\n\n::-webkit-scrollbar{\n    width: 0;\n    height: 0;\n}\n\n::-webkit-scrollbar-thumb{\n    background: #bfbfbf;\n    border-radius: 4px;\n}\n\n::-moz-selection,\n::selection {\n    color: black;\n    background: var(--primary-color);\n}\n\n/* make elements with attribute contenteditable editable on older iOS devices.\nSee note here: https://developer.mozilla.org/en-US/docs/Web/CSS/user-select */\n[contenteditable] {\n    -webkit-user-select: text;\n    user-select: text;\n}\n\n"
  },
  {
    "path": "rtc_config_example.json",
    "content": "{\n  \"sdpSemantics\": \"unified-plan\",\n  \"iceServers\": [\n    {\n      \"urls\": \"stun:<DOMAIN>:3478\"\n    },\n    {\n      \"urls\": \"turns:<DOMAIN>:5349\",\n      \"username\": \"username\",\n      \"credential\": \"password\"\n    }\n  ]\n}\n"
  },
  {
    "path": "server/helper.js",
    "content": "import crypto from \"crypto\";\n\nexport const hasher = (() => {\n    let password;\n    return {\n        hashCodeSalted(salt) {\n            if (!password) {\n                // password is created on first call.\n                password = randomizer.getRandomString(128);\n            }\n\n            return crypto.createHash(\"sha3-512\")\n                .update(password)\n                .update(crypto.createHash(\"sha3-512\").update(salt, \"utf8\").digest(\"hex\"))\n                .digest(\"hex\");\n        }\n    }\n})()\n\nexport const randomizer = (() => {\n    let charCodeLettersOnly = r => 65 <= r && r <= 90;\n    let charCodeAllPrintableChars = r => r === 45 || 47 <= r && r <= 57 || 64 <= r && r <= 90 || 97 <= r && r <= 122;\n\n    return {\n        getRandomString(length, lettersOnly = false) {\n            const charCodeCondition = lettersOnly\n                ? charCodeLettersOnly\n                : charCodeAllPrintableChars;\n\n            let string = \"\";\n            while (string.length < length) {\n                let arr = new Uint16Array(length);\n                crypto.webcrypto.getRandomValues(arr);\n                arr = Array.apply([], arr); /* turn into non-typed array */\n                arr = arr.map(function (r) {\n                    return r % 128\n                })\n                arr = arr.filter(function (r) {\n                    /* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */\n                    return charCodeCondition(r);\n                });\n                string += String.fromCharCode.apply(String, arr);\n            }\n            return string.substring(0, length)\n        }\n    }\n})()\n\n/*\n    cyrb53 (c) 2018 bryc (github.com/bryc)\n    A fast and simple hash function with decent collision resistance.\n    Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.\n    Public domain. Attribution appreciated.\n*/\nexport const cyrb53 = function(str, seed = 0) {\n    let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;\n    for (let i = 0, ch; i < str.length; i++) {\n        ch = str.charCodeAt(i);\n        h1 = Math.imul(h1 ^ ch, 2654435761);\n        h2 = Math.imul(h2 ^ ch, 1597334677);\n    }\n    h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);\n    h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);\n    return 4294967296 * (2097151 & h2) + (h1>>>0);\n};"
  },
  {
    "path": "server/index.js",
    "content": "import {spawn} from \"child_process\";\nimport fs from \"fs\";\n\nimport PairDropServer from \"./server.js\";\nimport PairDropWsServer from \"./ws-server.js\";\n\n// Handle SIGINT\nprocess.on('SIGINT', () => {\n    console.info(\"SIGINT Received, exiting...\")\n    process.exit(0)\n})\n\n// Handle SIGTERM\nprocess.on('SIGTERM', () => {\n    console.info(\"SIGTERM Received, exiting...\")\n    process.exit(0)\n})\n\n// Handle APP ERRORS\nprocess.on('uncaughtException', (error, origin) => {\n    console.log('----- Uncaught exception -----')\n    console.log(error)\n    console.log('----- Exception origin -----')\n    console.log(origin)\n})\nprocess.on('unhandledRejection', (reason, promise) => {\n    console.log('----- Unhandled Rejection at -----')\n    console.log(promise)\n    console.log('----- Reason -----')\n    console.log(reason)\n})\n\n// Evaluate arguments for deployment with Docker and Node.js\nlet conf = {};\n\nconf.debugMode = process.env.DEBUG_MODE === \"true\";\n\nconf.port = process.env.PORT || 3000;\n\nconf.wsFallback = process.argv.includes('--include-ws-fallback') || process.env.WS_FALLBACK === \"true\";\n\nconf.rtcConfig = process.env.RTC_CONFIG && process.env.RTC_CONFIG !== \"false\"\n    ? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))\n    : {\n        \"sdpSemantics\": \"unified-plan\",\n        \"iceServers\": [\n            {\n                \"urls\": \"stun:stun.l.google.com:19302\"\n            }\n        ]\n    };\n\n\nconf.signalingServer = process.env.SIGNALING_SERVER && process.env.SIGNALING_SERVER !== \"false\"\n    ? process.env.SIGNALING_SERVER\n    : false;\n\nconf.ipv6Localize = parseInt(process.env.IPV6_LOCALIZE) || false;\n\nlet rateLimit = false;\nif (process.argv.includes('--rate-limit') || process.env.RATE_LIMIT === \"true\") {\n    rateLimit = 5;\n}\nelse {\n    let envRateLimit = parseInt(process.env.RATE_LIMIT);\n    if (!isNaN(envRateLimit)) {\n        rateLimit = envRateLimit;\n    }\n}\nconf.rateLimit = rateLimit;\n\nconf.buttons = {\n    \"donation_button\": {\n        \"active\": process.env.DONATION_BUTTON_ACTIVE,\n        \"link\": process.env.DONATION_BUTTON_LINK,\n        \"title\": process.env.DONATION_BUTTON_TITLE\n    },\n    \"twitter_button\": {\n        \"active\": process.env.TWITTER_BUTTON_ACTIVE,\n        \"link\": process.env.TWITTER_BUTTON_LINK,\n        \"title\": process.env.TWITTER_BUTTON_TITLE\n    },\n    \"mastodon_button\": {\n        \"active\": process.env.MASTODON_BUTTON_ACTIVE,\n        \"link\": process.env.MASTODON_BUTTON_LINK,\n        \"title\": process.env.MASTODON_BUTTON_TITLE\n    },\n    \"bluesky_button\": {\n        \"active\": process.env.BLUESKY_BUTTON_ACTIVE,\n        \"link\": process.env.BLUESKY_BUTTON_LINK,\n        \"title\": process.env.BLUESKY_BUTTON_TITLE\n    },\n    \"custom_button\": {\n        \"active\": process.env.CUSTOM_BUTTON_ACTIVE,\n        \"link\": process.env.CUSTOM_BUTTON_LINK,\n        \"title\": process.env.CUSTOM_BUTTON_TITLE\n    },\n    \"privacypolicy_button\": {\n        \"active\": process.env.PRIVACYPOLICY_BUTTON_ACTIVE,\n        \"link\": process.env.PRIVACYPOLICY_BUTTON_LINK,\n        \"title\": process.env.PRIVACYPOLICY_BUTTON_TITLE\n    }\n};\n\n// Evaluate arguments for deployment with Node.js only\nconf.autoStart = process.argv.includes('--auto-restart');\n\nconf.localhostOnly = process.argv.includes('--localhost-only');\n\n\n// Validate configuration\nif (conf.ipv6Localize) {\n    if (!(0 < conf.ipv6Localize && conf.ipv6Localize < 8)) {\n        console.error(\"ipv6Localize must be an integer between 1 and 7\");\n        process.exit(1);\n    }\n\n    console.log(\"IPv6 client IPs will be localized to\",\n        conf.ipv6Localize,\n        conf.ipv6Localize === 1 ? \"segment\" : \"segments\");\n}\n\nif (conf.signalingServer) {\n    const isValidUrl = /[a-z|0-9|\\-._~:\\/?#\\[\\]@!$&'()*+,;=]+$/.test(conf.signalingServer);\n    const containsProtocol = /:\\/\\//.test(conf.signalingServer)\n    const endsWithSlash = /\\/$/.test(conf.signalingServer)\n    if (!isValidUrl || containsProtocol) {\n        console.error(\"SIGNALING_SERVER must be a valid url without the protocol prefix.\\n\" +\n            \"Examples of valid values: `pairdrop.net`, `pairdrop.example.com:3000`, `example.com/pairdrop`\");\n        process.exit(1);\n    }\n\n    if (!endsWithSlash) {\n        conf.signalingServer += \"/\";\n    }\n\n    if (process.env.RTC_CONFIG || conf.wsFallback || conf.ipv6Localize) {\n        console.error(\"SIGNALING_SERVER cannot be used alongside WS_FALLBACK, RTC_CONFIG or IPV6_LOCALIZE as these \" +\n            \"configurations are specified by the signaling server.\\n\" +\n            \"To use this instance as the signaling server do not set SIGNALING_SERVER\");\n        process.exit(1);\n    }\n}\n\n// Logs for debugging\nif (conf.debugMode) {\n    console.log(\"DEBUG_MODE is active. To protect privacy, do not use in production.\");\n    console.debug(\"\\n\");\n    console.debug(\"----DEBUG ENVIRONMENT VARIABLES----\")\n    console.debug(JSON.stringify(conf, null, 4));\n    console.debug(\"\\n\");\n}\n\n// Start a new PairDrop instance when an uncaught exception occurs\nif (conf.autoStart) {\n    process.on(\n        'uncaughtException',\n        () => {\n            process.once(\n                'exit',\n                () => spawn(\n                    process.argv.shift(),\n                    process.argv,\n                    {\n                        cwd: process.cwd(),\n                        detached: true,\n                        stdio: 'inherit'\n                    }\n                )\n            );\n            process.exit();\n        }\n    );\n}\n\n// Start server to serve client files\nconst pairDropServer = new PairDropServer(conf);\n\nif (!conf.signalingServer) {\n    // Start websocket server if SIGNALING_SERVER is not set\n    new PairDropWsServer(pairDropServer.server, conf);\n} else {\n    console.log(\"This instance does not include a signaling server. Clients on this instance connect to the following signaling server:\", conf.signalingServer);\n}\n\nconsole.log('\\nPairDrop is running on port', conf.port);\n"
  },
  {
    "path": "server/peer.js",
    "content": "import crypto from \"crypto\";\nimport parser from \"ua-parser-js\";\nimport {animals, colors, uniqueNamesGenerator} from \"unique-names-generator\";\nimport {cyrb53, hasher} from \"./helper.js\";\n\nexport default class Peer {\n\n    constructor(socket, request, conf) {\n        this.conf = conf\n\n        // set socket\n        this.socket = socket;\n\n        // set remote ip\n        this._setIP(request);\n\n        // set peer id\n        this._setPeerId(request);\n\n        // is WebRTC supported\n        this._setRtcSupported(request);\n\n        // set name\n        this._setName(request);\n\n        this.requestRate = 0;\n\n        this.roomSecrets = [];\n        this.pairKey = null;\n\n        this.publicRoomId = null;\n    }\n\n    rateLimitReached() {\n        // rate limit implementation: max 10 attempts every 10s\n        if (this.requestRate >= 10) {\n            return true;\n        }\n        this.requestRate += 1;\n        setTimeout(() => this.requestRate -= 1, 10000);\n        return false;\n    }\n\n    _setIP(request) {\n        if (request.headers['cf-connecting-ip']) {\n            this.ip = request.headers['cf-connecting-ip'].split(/\\s*,\\s*/)[0];\n        }\n        else if (request.headers['x-forwarded-for']) {\n            this.ip = request.headers['x-forwarded-for'].split(/\\s*,\\s*/)[0];\n        }\n        else {\n            this.ip = request.socket.remoteAddress ?? '';\n        }\n\n        // remove the prefix used for IPv4-translated addresses\n        if (this.ip.substring(0,7) === \"::ffff:\") {\n            this.ip = this.ip.substring(7);\n        }\n\n        let ipv6_was_localized = false;\n        if (this.conf.ipv6Localize && this.ip.includes(':')) {\n            this.ip = this.ip.split(':',this.conf.ipv6Localize).join(':');\n            ipv6_was_localized = true;\n        }\n\n        if (this.conf.debugMode) {\n            console.debug(\"\\n\");\n            console.debug(\"----DEBUGGING-PEER-IP-START----\");\n            console.debug(\"remoteAddress:\", request.connection.remoteAddress);\n            console.debug(\"x-forwarded-for:\", request.headers['x-forwarded-for']);\n            console.debug(\"cf-connecting-ip:\", request.headers['cf-connecting-ip']);\n            if (ipv6_was_localized) {\n                console.debug(\"IPv6 client IP was localized to\", this.conf.ipv6Localize, this.conf.ipv6Localize > 1 ? \"segments\" : \"segment\");\n            }\n            console.debug(\"PairDrop uses:\", this.ip);\n            console.debug(\"IP is private:\", this.ipIsPrivate(this.ip));\n            console.debug(\"if IP is private, '127.0.0.1' is used instead\");\n            console.debug(\"----DEBUGGING-PEER-IP-END----\");\n        }\n\n        // IPv4 and IPv6 use different values to refer to localhost\n        // put all peers on the same network as the server into the same room as well\n        if (this.ip === '::1' || this.ipIsPrivate(this.ip)) {\n            this.ip = '127.0.0.1';\n        }\n    }\n\n    ipIsPrivate(ip) {\n        // if ip is IPv4\n        if (!ip.includes(\":\")) {\n            //         10.0.0.0 - 10.255.255.255        ||   172.16.0.0 - 172.31.255.255                          ||    192.168.0.0 - 192.168.255.255\n            return /^(10)\\.(.*)\\.(.*)\\.(.*)$/.test(ip) || /^(172)\\.(1[6-9]|2[0-9]|3[0-1])\\.(.*)\\.(.*)$/.test(ip) || /^(192)\\.(168)\\.(.*)\\.(.*)$/.test(ip)\n        }\n\n        // else: ip is IPv6\n        const firstWord = ip.split(\":\").find(el => !!el); //get first not empty word\n\n        if (/^fe[c-f][0-f]$/.test(firstWord)) {\n            // The original IPv6 Site Local addresses (fec0::/10) are deprecated. Range: fec0 - feff\n            return true;\n        }\n\n        // These days Unique Local Addresses (ULA) are used in place of Site Local.\n        // Range: fc00 - fcff\n        else if (/^fc[0-f]{2}$/.test(firstWord)) {\n            return true;\n        }\n\n        // Range: fd00 - fcff\n        else if (/^fd[0-f]{2}$/.test(firstWord)) {\n            return true;\n        }\n\n        // Link local addresses (prefixed with fe80) are not routable\n        else if (firstWord === \"fe80\") {\n            return true;\n        }\n\n        // Discard Prefix\n        else if (firstWord === \"100\") {\n            return true;\n        }\n\n        // Any other IP address is not Unique Local Address (ULA)\n        return false;\n    }\n\n    _setPeerId(request) {\n        const searchParams = new URL(request.url, \"http://server\").searchParams;\n        let peerId = searchParams.get('peer_id');\n        let peerIdHash = searchParams.get('peer_id_hash');\n        if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {\n            this.id = peerId;\n        } else {\n            this.id = crypto.randomUUID();\n        }\n    }\n\n    _setRtcSupported(request) {\n        const searchParams = new URL(request.url, \"http://server\").searchParams;\n        this.rtcSupported = searchParams.get('webrtc_supported') === \"true\";\n    }\n\n    _setName(req) {\n        let ua = parser(req.headers['user-agent']);\n\n        let deviceName = '';\n\n        if (ua.os && ua.os.name) {\n            deviceName = ua.os.name.replace('Mac OS', 'Mac') + ' ';\n        }\n\n        if (ua.device.model) {\n            deviceName += ua.device.model;\n        } else {\n            deviceName += ua.browser.name;\n        }\n\n        if (!deviceName) {\n            deviceName = 'Unknown Device';\n        }\n\n        const displayName = uniqueNamesGenerator({\n            length: 2,\n            separator: ' ',\n            dictionaries: [colors, animals],\n            style: 'capital',\n            seed: cyrb53(this.id)\n        })\n\n        this.name = {\n            model: ua.device.model,\n            os: ua.os.name,\n            browser: ua.browser.name,\n            type: ua.device.type,\n            deviceName,\n            displayName\n        };\n    }\n\n    getInfo() {\n        return {\n            id: this.id,\n            name: this.name,\n            rtcSupported: this.rtcSupported\n        }\n    }\n\n    static isValidUuid(uuid) {\n        return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);\n    }\n\n    isPeerIdHashValid(peerId, peerIdHash) {\n        return peerIdHash === hasher.hashCodeSalted(peerId);\n    }\n\n    addRoomSecret(roomSecret) {\n        if (!(roomSecret in this.roomSecrets)) {\n            this.roomSecrets.push(roomSecret);\n        }\n    }\n\n    removeRoomSecret(roomSecret) {\n        if (roomSecret in this.roomSecrets) {\n            delete this.roomSecrets[roomSecret];\n        }\n    }\n}"
  },
  {
    "path": "server/server.js",
    "content": "import express from \"express\";\nimport RateLimit from \"express-rate-limit\";\nimport {fileURLToPath} from \"url\";\nimport path, {dirname} from \"path\";\nimport http from \"http\";\n\nexport default class PairDropServer {\n\n    constructor(conf) {\n        const app = express();\n\n        if (conf.rateLimit) {\n            const limiter = RateLimit({\n                windowMs: 5 * 60 * 1000, // 5 minutes\n                max: 1000, // Limit each IP to 1000 requests per `window` (here, per 5 minutes)\n                message: 'Too many requests from this IP Address, please try again after 5 minutes.',\n                standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers\n                legacyHeaders: false, // Disable the `X-RateLimit-*` headers\n            })\n\n            app.use(limiter);\n            // ensure correct client ip and not the ip of the reverse proxy is used for rate limiting\n            // see https://express-rate-limit.mintlify.app/guides/troubleshooting-proxy-issues\n\n            app.set('trust proxy', conf.rateLimit);\n\n            if (!conf.debugMode) {\n                console.log(\"Use DEBUG_MODE=true to find correct number for RATE_LIMIT.\");\n            }\n        }\n\n        const __filename = fileURLToPath(import.meta.url);\n        const __dirname = dirname(__filename);\n\n        const publicPathAbs = path.join(__dirname, '../public');\n        app.use(express.static(publicPathAbs));\n\n        if (conf.debugMode && conf.rateLimit) {\n            console.debug(\"\\n\");\n            console.debug(\"----DEBUG RATE_LIMIT----\")\n            console.debug(\"To find out the correct value for RATE_LIMIT go to '/ip' and ensure the returned IP-address is the IP-address of your client.\")\n            console.debug(\"See https://github.com/express-rate-limit/express-rate-limit#troubleshooting-proxy-issues for more info\")\n            app.get('/ip', (req, res) => {\n                res.send(req.ip);\n            })\n        }\n\n        // By default, clients connecting to your instance use the signaling server of your instance to connect to other devices.\n        // By using `WS_SERVER`, you can host an instance that uses another signaling server.\n        app.get('/config', (req, res) => {\n            res.send({\n                signalingServer: conf.signalingServer,\n                buttons: conf.buttons\n            });\n        });\n\n        app.use((req, res) => {\n            res.redirect(301, '/');\n        });\n\n        app.get('/', (req, res) => {\n            res.sendFile('index.html');\n            console.log(`Serving client files from:\\n${publicPathAbs}`)\n        });\n\n        const hostname = conf.localhostOnly ? '127.0.0.1' : null;\n        const server = http.createServer(app);\n\n        server.listen(conf.port, hostname);\n\n        server.on('error', (err) => {\n            if (err.code === 'EADDRINUSE') {\n                console.error(err);\n                console.info(\"Error EADDRINUSE received, exiting process without restarting process...\");\n                process.exit(1)\n            }\n        });\n\n        this.server = server\n    }\n}"
  },
  {
    "path": "server/ws-server.js",
    "content": "import {WebSocketServer} from \"ws\";\nimport crypto from \"crypto\"\n\nimport Peer from \"./peer.js\";\nimport {hasher, randomizer} from \"./helper.js\";\n\nexport default class PairDropWsServer {\n\n    constructor(server, conf) {\n        this._conf = conf\n\n        this._rooms = {}; // { roomId: peers[] }\n\n        this._roomSecrets = {}; // { pairKey: roomSecret }\n        this._keepAliveTimers = {};\n\n        this._wss = new WebSocketServer({ server });\n        this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request, conf)));\n    }\n\n    _onConnection(peer) {\n        peer.socket.on('message', message => this._onMessage(peer, message));\n        peer.socket.onerror = e => console.error(e);\n\n        this._keepAlive(peer);\n\n        this._send(peer, {\n            type: 'ws-config',\n            wsConfig: {\n                rtcConfig: this._conf.rtcConfig,\n                wsFallback: this._conf.wsFallback\n            }\n        });\n\n        // send displayName\n        this._send(peer, {\n            type: 'display-name',\n            displayName: peer.name.displayName,\n            deviceName: peer.name.deviceName,\n            peerId: peer.id,\n            peerIdHash: hasher.hashCodeSalted(peer.id)\n        });\n    }\n\n    _onMessage(sender, message) {\n        // Try to parse message\n        try {\n            message = JSON.parse(message);\n        } catch (e) {\n            console.warn(\"WS: Received JSON is malformed\");\n            return;\n        }\n\n        switch (message.type) {\n            case 'disconnect':\n                this._onDisconnect(sender);\n                break;\n            case 'pong':\n                this._setKeepAliveTimerToNow(sender);\n                break;\n            case 'join-ip-room':\n                this._joinIpRoom(sender);\n                break;\n            case 'room-secrets':\n                this._onRoomSecrets(sender, message);\n                break;\n            case 'room-secrets-deleted':\n                this._onRoomSecretsDeleted(sender, message);\n                break;\n            case 'pair-device-initiate':\n                this._onPairDeviceInitiate(sender);\n                break;\n            case 'pair-device-join':\n                this._onPairDeviceJoin(sender, message);\n                break;\n            case 'pair-device-cancel':\n                this._onPairDeviceCancel(sender);\n                break;\n            case 'regenerate-room-secret':\n                this._onRegenerateRoomSecret(sender, message);\n                break;\n            case 'create-public-room':\n                this._onCreatePublicRoom(sender);\n                break;\n            case 'join-public-room':\n                this._onJoinPublicRoom(sender, message);\n                break;\n            case 'leave-public-room':\n                this._onLeavePublicRoom(sender);\n                break;\n            case 'signal':\n                this._signalAndRelay(sender, message);\n                break;\n            case 'request':\n            case 'header':\n            case 'partition':\n            case 'partition-received':\n            case 'progress':\n            case 'files-transfer-response':\n            case 'file-transfer-complete':\n            case 'message-transfer-complete':\n            case 'text':\n            case 'display-name-changed':\n            case 'ws-chunk':\n                // relay ws-fallback\n                if (this._conf.wsFallback) {\n                    this._signalAndRelay(sender, message);\n                }\n                else {\n                    console.log(\"Websocket fallback is not activated on this instance.\")\n                }\n        }\n    }\n\n    _signalAndRelay(sender, message) {\n        const room = message.roomType === 'ip'\n            ? sender.ip\n            : message.roomId;\n\n        // relay message to recipient\n        if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) {\n            const recipient = this._rooms[room][message.to];\n            delete message.to;\n            // add sender\n            message.sender = {\n                id: sender.id,\n                rtcSupported: sender.rtcSupported\n            };\n            this._send(recipient, message);\n        }\n    }\n\n    _onDisconnect(sender) {\n        this._disconnect(sender);\n    }\n\n    _disconnect(sender) {\n        this._removePairKey(sender.pairKey);\n        sender.pairKey = null;\n\n        this._cancelKeepAlive(sender);\n        delete this._keepAliveTimers[sender.id];\n\n        this._leaveIpRoom(sender, true);\n        this._leaveAllSecretRooms(sender, true);\n        this._leavePublicRoom(sender, true);\n\n        sender.socket.terminate();\n    }\n\n    _onRoomSecrets(sender, message) {\n        if (!message.roomSecrets) return;\n\n        const roomSecrets = message.roomSecrets.filter(roomSecret => {\n            return /^[\\x00-\\x7F]{64,256}$/.test(roomSecret);\n        })\n\n        if (!roomSecrets) return;\n\n        this._joinSecretRooms(sender, roomSecrets);\n    }\n\n    _onRoomSecretsDeleted(sender, message) {\n        for (let i = 0; i<message.roomSecrets.length; i++) {\n            this._deleteSecretRoom(message.roomSecrets[i]);\n        }\n    }\n\n    _deleteSecretRoom(roomSecret) {\n        const room = this._rooms[roomSecret];\n        if (!room) return;\n\n        for (const peerId in room) {\n            const peer = room[peerId];\n\n            this._leaveSecretRoom(peer, roomSecret, true);\n\n            this._send(peer, {\n                type: 'secret-room-deleted',\n                roomSecret: roomSecret,\n            });\n        }\n    }\n\n    _onPairDeviceInitiate(sender) {\n        let roomSecret = randomizer.getRandomString(256);\n        let pairKey = this._createPairKey(sender, roomSecret);\n\n        if (sender.pairKey) {\n            this._removePairKey(sender.pairKey);\n        }\n        sender.pairKey = pairKey;\n\n        this._send(sender, {\n            type: 'pair-device-initiated',\n            roomSecret: roomSecret,\n            pairKey: pairKey\n        });\n        this._joinSecretRoom(sender, roomSecret);\n    }\n\n    _onPairDeviceJoin(sender, message) {\n        if (sender.rateLimitReached()) {\n            this._send(sender, { type: 'join-key-rate-limit' });\n            return;\n        }\n\n        if (!this._roomSecrets[message.pairKey] || sender.id === this._roomSecrets[message.pairKey].creator.id) {\n            this._send(sender, { type: 'pair-device-join-key-invalid' });\n            return;\n        }\n\n        const roomSecret = this._roomSecrets[message.pairKey].roomSecret;\n        const creator = this._roomSecrets[message.pairKey].creator;\n        this._removePairKey(message.pairKey);\n        this._send(sender, {\n            type: 'pair-device-joined',\n            roomSecret: roomSecret,\n            peerId: creator.id\n        });\n        this._send(creator, {\n            type: 'pair-device-joined',\n            roomSecret: roomSecret,\n            peerId: sender.id\n        });\n        this._joinSecretRoom(sender, roomSecret);\n        this._removePairKey(sender.pairKey);\n    }\n\n    _onPairDeviceCancel(sender) {\n        const pairKey = sender.pairKey\n\n        if (!pairKey) return;\n\n        this._removePairKey(pairKey);\n        this._send(sender, {\n            type: 'pair-device-canceled',\n            pairKey: pairKey,\n        });\n    }\n\n    _onCreatePublicRoom(sender) {\n        let publicRoomId = randomizer.getRandomString(5, true).toLowerCase();\n\n        this._send(sender, {\n            type: 'public-room-created',\n            roomId: publicRoomId\n        });\n\n        this._joinPublicRoom(sender, publicRoomId);\n    }\n\n    _onJoinPublicRoom(sender, message) {\n        if (sender.rateLimitReached()) {\n            this._send(sender, { type: 'join-key-rate-limit' });\n            return;\n        }\n\n        if (!this._rooms[message.publicRoomId] && !message.createIfInvalid) {\n            this._send(sender, { type: 'public-room-id-invalid', publicRoomId: message.publicRoomId });\n            return;\n        }\n\n        this._leavePublicRoom(sender);\n        this._joinPublicRoom(sender, message.publicRoomId);\n    }\n\n    _onLeavePublicRoom(sender) {\n        this._leavePublicRoom(sender, true);\n        this._send(sender, { type: 'public-room-left' });\n    }\n\n    _onRegenerateRoomSecret(sender, message) {\n        const oldRoomSecret = message.roomSecret;\n        const newRoomSecret = randomizer.getRandomString(256);\n\n        // notify all other peers\n        for (const peerId in this._rooms[oldRoomSecret]) {\n            const peer = this._rooms[oldRoomSecret][peerId];\n            this._send(peer, {\n                type: 'room-secret-regenerated',\n                oldRoomSecret: oldRoomSecret,\n                newRoomSecret: newRoomSecret,\n            });\n            peer.removeRoomSecret(oldRoomSecret);\n        }\n        delete this._rooms[oldRoomSecret];\n    }\n\n    _createPairKey(creator, roomSecret) {\n        let pairKey;\n        do {\n            // get randomInt until keyRoom not occupied\n            pairKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s\n        } while (pairKey in this._roomSecrets)\n\n        this._roomSecrets[pairKey] = {\n            roomSecret: roomSecret,\n            creator: creator\n        }\n\n        return pairKey;\n    }\n\n    _removePairKey(pairKey) {\n        if (pairKey in this._roomSecrets) {\n            this._roomSecrets[pairKey].creator.pairKey = null\n            delete this._roomSecrets[pairKey];\n        }\n    }\n\n    _joinIpRoom(peer) {\n        this._joinRoom(peer, 'ip', peer.ip);\n    }\n\n    _joinSecretRoom(peer, roomSecret) {\n        this._joinRoom(peer, 'secret', roomSecret);\n\n        // add secret to peer\n        peer.addRoomSecret(roomSecret);\n    }\n\n    _joinPublicRoom(peer, publicRoomId) {\n        // prevent joining of 2 public rooms simultaneously\n        this._leavePublicRoom(peer);\n\n        this._joinRoom(peer, 'public-id', publicRoomId);\n\n        peer.publicRoomId = publicRoomId;\n    }\n\n    _joinRoom(peer, roomType, roomId) {\n        // roomType: 'ip', 'secret' or 'public-id'\n        if (this._rooms[roomId] && this._rooms[roomId][peer.id]) {\n            // ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect.\n            this._leaveRoom(peer, roomType, roomId);\n        }\n\n        // if room doesn't exist, create it\n        if (!this._rooms[roomId]) {\n            this._rooms[roomId] = {};\n        }\n\n        this._notifyPeers(peer, roomType, roomId);\n\n        // add peer to room\n        this._rooms[roomId][peer.id] = peer;\n    }\n\n\n    _leaveIpRoom(peer, disconnect = false) {\n        this._leaveRoom(peer, 'ip', peer.ip, disconnect);\n    }\n\n    _leaveSecretRoom(peer, roomSecret, disconnect = false) {\n        this._leaveRoom(peer, 'secret', roomSecret, disconnect)\n\n        //remove secret from peer\n        peer.removeRoomSecret(roomSecret);\n    }\n\n    _leavePublicRoom(peer, disconnect = false) {\n        if (!peer.publicRoomId) return;\n\n        this._leaveRoom(peer, 'public-id', peer.publicRoomId, disconnect);\n\n        peer.publicRoomId = null;\n    }\n\n    _leaveRoom(peer, roomType, roomId, disconnect = false) {\n        if (!this._rooms[roomId] || !this._rooms[roomId][peer.id]) return;\n\n        // remove peer from room\n        delete this._rooms[roomId][peer.id];\n\n        // delete room if empty and abort\n        if (!Object.keys(this._rooms[roomId]).length) {\n            delete this._rooms[roomId];\n            return;\n        }\n\n        // notify all other peers that remain in room that peer left\n        for (const otherPeerId in this._rooms[roomId]) {\n            const otherPeer = this._rooms[roomId][otherPeerId];\n\n            let msg = {\n                type: 'peer-left',\n                peerId: peer.id,\n                roomType: roomType,\n                roomId: roomId,\n                disconnect: disconnect\n            };\n\n            this._send(otherPeer, msg);\n        }\n    }\n\n    _notifyPeers(peer, roomType, roomId) {\n        if (!this._rooms[roomId]) return;\n\n        // notify all other peers that peer joined\n        for (const otherPeerId in this._rooms[roomId]) {\n            if (otherPeerId === peer.id) continue;\n            const otherPeer = this._rooms[roomId][otherPeerId];\n\n            let msg = {\n                type: 'peer-joined',\n                peer: peer.getInfo(),\n                roomType: roomType,\n                roomId: roomId\n            };\n\n            this._send(otherPeer, msg);\n        }\n\n        // notify peer about peers already in the room\n        const otherPeers = [];\n        for (const otherPeerId in this._rooms[roomId]) {\n            if (otherPeerId === peer.id) continue;\n            otherPeers.push(this._rooms[roomId][otherPeerId].getInfo());\n        }\n\n        let msg = {\n            type: 'peers',\n            peers: otherPeers,\n            roomType: roomType,\n            roomId: roomId\n        };\n\n        this._send(peer, msg);\n    }\n\n    _joinSecretRooms(peer, roomSecrets) {\n        for (let i=0; i<roomSecrets.length; i++) {\n            this._joinSecretRoom(peer, roomSecrets[i])\n        }\n    }\n\n    _leaveAllSecretRooms(peer, disconnect = false) {\n        for (let i=0; i<peer.roomSecrets.length; i++) {\n            this._leaveSecretRoom(peer, peer.roomSecrets[i], disconnect);\n        }\n    }\n\n    _send(peer, message) {\n        if (!peer) return;\n        if (this._wss.readyState !== this._wss.OPEN) return;\n        message = JSON.stringify(message);\n        peer.socket.send(message);\n    }\n\n    _keepAlive(peer) {\n        this._cancelKeepAlive(peer);\n        let timeout = 1000;\n\n        if (!this._keepAliveTimers[peer.id]) {\n            this._keepAliveTimers[peer.id] = {\n                timer: 0,\n                lastBeat: Date.now()\n            };\n        }\n\n        if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 5 * timeout) {\n            // Disconnect peer if unresponsive for 10s\n            this._disconnect(peer);\n            return;\n        }\n\n        this._send(peer, { type: 'ping' });\n\n        this._keepAliveTimers[peer.id].timer = setTimeout(() => this._keepAlive(peer), timeout);\n    }\n\n    _cancelKeepAlive(peer) {\n        if (this._keepAliveTimers[peer.id]?.timer) {\n            clearTimeout(this._keepAliveTimers[peer.id].timer);\n        }\n    }\n\n    _setKeepAliveTimerToNow(peer) {\n        if (this._keepAliveTimers[peer.id]?.lastBeat) {\n            this._keepAliveTimers[peer.id].lastBeat = Date.now();\n        }\n    }\n}\n\n"
  },
  {
    "path": "turnserver_example.conf",
    "content": "# TURN server name and realm\nrealm=<DOMAIN>\nserver-name=pairdrop\n\n# IPs the TURN server listens to\nlistening-ip=0.0.0.0\n\n# External IP-Address of the TURN server\n# only needed, if coturn is behind a NAT\n# external-ip=<IP_ADDRESS>\n\n# Main listening port for STUN and TURN\nlistening-port=3478\n\n# Main listening port for TURN over TLS (TURNS)\n# Use port 443 to bypass some firewalls\ntls-listening-port=5349\n\n# Further ports that are open for communication\nmin-port=10000\nmax-port=20000\n\n# Use fingerprint in TURN message\nfingerprint\n\n# Enable verbose logging\n# verbose\n\n# Log file path\n# - is logging to STDOUT, so it's visible in docker-compose logs\nlog-file=-\n\n# Specify the user for the TURN authentification\nuser=username:password\n\n# Enable long-term credential mechanism\nlt-cred-mech\n\n# SSL certificates\ncert=/etc/coturn/ssl/cert.crt\npkey=/etc/coturn/ssl/pkey.pem\ndh-file=/etc/coturn/ssl/dhparam.pem\n\n# For security-reasons disable old ssl and tls-protocols\n# and other recommended options: see https://github.com/coturn/coturn/blob/master/examples/etc/turnserver.conf\nno-sslv3\nno-tlsv1\nno-tlsv1_1\nno-tlsv1_2\nno-rfc5780\nno-stun-backward-compatibility\nresponse-origin-only-with-rfc5780\nno-cli\nno-multicast-peers\nno-software-attribute\ncheck-origin-consistency"
  }
]