diff options
97 files changed, 1969 insertions, 892 deletions
diff --git a/.env.production.sample b/.env.production.sample index 9011dc217..28afee246 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -35,11 +35,14 @@ OTP_SECRET= # E-mail configuration # Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers +# If you want to use an SMTP server without authentication (e.g local Postfix relay) +# then set SMTP_AUTH_METHOD to 'none' and leave SMTP_LOGIN and SMTP_PASSWORD blank SMTP_SERVER=smtp.mailgun.org SMTP_PORT=587 SMTP_LOGIN= SMTP_PASSWORD= SMTP_FROM_ADDRESS=notifications@example.com +#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN #SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail #SMTP_AUTH_METHOD=plain #SMTP_OPENSSL_VERIFY_MODE=peer @@ -81,3 +84,7 @@ SMTP_FROM_ADDRESS=notifications@example.com # Advanced settings # If you need to use pgBouncer, you need to disable prepared statements: # PREPARED_STATEMENTS=false + +# Cluster number setting for streaming API server. +# If you comment out following line, cluster number will be `numOfCpuCores - 1`. +STREAMING_CLUSTER_NUM=1 diff --git a/.gitignore b/.gitignore index cda6b87b3..c6c468cc7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,9 @@ # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' -# Ignore bundler config. +# Ignore bundler config and downloaded libraries. /.bundle +/vendor/bundle # Ignore the default SQLite database. /db/*.sqlite3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ca01a56f..299306299 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ There are three ways in which you can contribute to this repository: 2. By working on the back-end application 3. By working on the front-end application -Choosing what to work on in a large open source project is not easy. The list of GitHub issues may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise. If your addition creates a new feature or setting, or otherwise changes how things work in some substantial way, please remember to submit a correlating pull request to document your changes in the [documentation](http://github.com/tootsuite/documentation). +Choosing what to work on in a large open source project is not easy. The list of [GitHub issues](https://github.com/tootsuite/mastodon/issues) may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise. If your addition creates a new feature or setting, or otherwise changes how things work in some substantial way, please remember to submit a correlating pull request to document your changes in the [documentation](http://github.com/tootsuite/documentation). Below are the guidelines for working on pull requests: @@ -21,9 +21,17 @@ Below are the guidelines for working on pull requests: - No orthographic mistakes - No Markdown syntax errors +## Requirements + +- Ruby +- Node.js +- PostgreSQL +- Redis +- Nginx (optional) + ## Back-end application -It is expected that you have a working development environment set up. The development environment includes rubocop, which checks your Ruby code for compliance with our style guide and best practices. Sublime Text, likely like other editors, has a Rubocop plugin that runs checks on files as you edit them. The codebase also has a test suite. +It is expected that you have a working development environment set up. The development environment includes [rubocop](https://github.com/bbatsov/rubocop), which checks your Ruby code for compliance with our style guide and best practices. Sublime Text, likely like other editors, has a [Rubocop plugin](https://github.com/pderichs/sublime_rubocop) that runs checks on files as you edit them. The codebase also has a test suite. * The codebase is not perfect, at the time of writing, but it is expected that you do not introduce new code style violations * The rspec test suite must pass @@ -41,4 +49,3 @@ It is expected that you have a working development environment set up (see back- * If you are introducing new strings, they must be using localization methods If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet. - diff --git a/README.md b/README.md index ea7b20a5c..804ad394b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Mastodon Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly. -An alternative implementation of the GNU social project. Based on ActivityStreams, Webfinger, PubsubHubbub and Salmon. +An alternative implementation of the GNU social project. Based on [ActivityStreams](https://en.wikipedia.org/wiki/Activity_Streams_(format)), [Webfinger](https://en.wikipedia.org/wiki/WebFinger), [PubsubHubbub](https://en.wikipedia.org/wiki/PubSubHubbub) and [Salmon](https://en.wikipedia.org/wiki/Salmon_(protocol)). Click on the screenshot to watch a demo of the UI: @@ -48,126 +48,9 @@ If you would like, you can [support the development of this project on Patreon][ - **Deployable via Docker** You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy -## Checking out +## Deployment -If you want a stable release for production use, you should use tagged releases. To checkout the latest available tagged version: - - git clone https://github.com/tootsuite/mastodon.git - cd mastodon - git checkout $(git describe --tags `git rev-list --tags --max-count=1`) - -## Configuration - -- `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related -- `LOCAL_HTTPS` set it to `true` if HTTPS works on your website. This is used to generate canonical URLs, which is also important when generating and parsing federation-related IDs - -Consult the example configuration file, `.env.production.sample` for the full list. Among other things you need to set details for the SMTP server you are going to use. - -## Requirements - -- Ruby -- Node.js -- PostgreSQL -- Redis -- Nginx - -## Running with Docker and Docker-Compose - -[![](https://images.microbadger.com/badges/version/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com") - -The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`). - -Review the settings in `docker-compose.yml`. Note that it is not default to store the postgresql database and redis databases in a persistent storage location, -so you may need or want to adjust the settings there. - -Then, you need to fill in the `.env.production` file: - - cp .env.production.sample .env.production - nano .env.production - -Do NOT change the `REDIS_*` or `DB_*` settings when running with the default docker configurations. - -You will need to fill in, at least: `LOCAL_DOMAIN`, `LOCAL_HTTPS`, `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, `OTP_SECRET`, and the `SMTP_*` settings. To generate the `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, and `OTP_SECRET`, you may use: - -Before running the first time, you need to build the images: - - docker-compose build - - - docker-compose run --rm web rake secret - -Do this once for each of those keys, and copy the result into the `.env.production` file in the appropriate field. - -Then you should run the `db:migrate` command to create the database, or migrate it from an older release: - - docker-compose run --rm web rails db:migrate - -Then, you will also need to precompile the assets: - - docker-compose run --rm web rails assets:precompile - -before you can launch the docker image with: - - docker-compose up - -If you wish to run this as a daemon process instead of monitoring it on console, use instead: - - docker-compose up -d - -Then you may login to your new Mastodon instance by browsing to http://localhost:3000/ - -Following that, make sure that you read the [production guide](docs/Running-Mastodon/Production-guide.md). You are probably going to want to understand how -to configure Nginx to make your Mastodon instance available to the rest of the world. - -The container has two volumes, for the assets and for user uploads, and optionally two more, for the postgresql and redis databases. - -The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up. - -**Note**: The `--rm` option for docker-compose will remove the container that is created to run a one-off command after it completes. As data is stored in volumes it is not affected by that container clean-up. - -### Tasks - -- `rake mastodon:media:clear` removes uploads that have not been attached to any status after a while, you would want to run this from a periodic cronjob -- `rake mastodon:push:clear` unsubscribes from PuSH notifications for remote users that have no local followers. You may not want to actually do that, to keep a fuller footprint of the fediverse or in case your users will soon re-follow -- `rake mastodon:push:refresh` re-subscribes PuSH for expiring remote users, this should be run periodically from a cronjob and quite often as the expiration time depends on the particular hub of the remote user -- `rake mastodon:feeds:clear_all` removes all timelines, which forces them to be re-built on the fly next time a user tries to fetch their home/mentions timeline. Only for troubleshooting -- `rake mastodon:feeds:clear` removes timelines of users who haven't signed in lately, which allows to save RAM and improve message distribution. This is required to be run periodically so that when they login again the regeneration process will trigger - -Running any of these tasks via docker-compose would look like this: - - docker-compose run --rm web rake mastodon:media:clear - -### Updating - -This approach makes updating to the latest version a real breeze. - -1. `git pull` to download updates from the repository -2. `docker-compose build` to compile the Docker image out of the changed source files -3. (optional) `docker-compose run --rm web rails db:migrate` to perform database migrations. Does nothing if your database is up to date -4. (optional) `docker-compose run --rm web rails assets:precompile` to compile new JS and CSS assets -5. `docker-compose up -d` to re-create (restart) containers and pick up the changes - -## Deployment without Docker - -Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md) for examples, configuration and instructions. - -## Deployment on Scalingo - -[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master) - -[You can view a guide for deployment on Scalingo here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md) - -## Deployment on Heroku (experimental) - -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) - -Mastodon can run on [Heroku](https://heroku.com), but it gets expensive and impractical due to how Heroku prices resource usage. [You can view a guide for deployment on Heroku here](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md), but you have been warned. - -## Development with Vagrant - -A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed. - -[You can find the guide for setting up a Vagrant development environment here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md) +There are guides in the documentation repository for [deploying on various platforms](https://github.com/tootsuite/documentation#running-mastodon). ## Contributing diff --git a/Vagrantfile b/Vagrantfile index 66892e443..9047037bc 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -107,7 +107,11 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.hostsupdater.remove_on_suspend = false end - config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp'] + if config.vm.networks.any? { |type, options| type == :private_network } + config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp'] + else + config.vm.synced_folder ".", "/vagrant" + end # Otherwise, you can access the site at http://localhost:3000 config.vm.network :forwarded_port, guest: 80, host: 3000 diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png index 3ed93f120..f0c1c46c3 100644 --- a/app/assets/images/logo.png +++ b/app/assets/images/logo.png Binary files differdiff --git a/app/assets/images/logo.svg b/app/assets/images/logo.svg index 52bf86b0e..c233db842 100644 --- a/app/assets/images/logo.svg +++ b/app/assets/images/logo.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><g fill="#189efc"><path d="M500 0A500 500 0 0 0 0 500a500 500 0 0 0 500 500 500 500 0 0 0 500-500A500 500 0 0 0 500 0zm-2.5 271.1h107.24c-20.56 14.471-27.24 57.064-27.24 78.927v202.145c0 43.726-35.202 78.928-80 78.928s-80-35.202-80-78.928V350.027c0-43.725 35.202-78.927 80-78.927zm-276 48.9c44.798 0 80 35.202 80 78.928v202.144c0 21.863 6.68 64.456 27.24 78.928H221.5c-44.798 0-80-35.202-80-78.928V398.928c0-43.726 35.202-78.928 80-78.928zm550.24 0c44.799 0 80 35.202 80 78.928v202.144c0 43.726-35.201 78.928-80 78.928H664.5c20.56-14.472 27.24-57.065 27.24-78.928V398.928c0-43.726 35.202-78.928 80-78.928z"/><g transform="translate(-2)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(1 0 0 -1 274 951)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(-1 0 0 1 995 0)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g></g></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#189efc"/></svg> \ No newline at end of file diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index 88e91c356..de75ddabe 100644 --- a/app/assets/javascripts/components/actions/compose.jsx +++ b/app/assets/javascripts/components/actions/compose.jsx @@ -73,9 +73,13 @@ export function mentionCompose(account, router) { export function submitCompose() { return function (dispatch, getState) { + const status = emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')); + if (!status || !status.length) { + return; + } dispatch(submitComposeRequest()); api(getState).post('/api/v1/statuses', { - status: emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')), + status, in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')), sensitive: getState().getIn(['compose', 'sensitive']), diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 08576913e..185911861 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -57,6 +57,7 @@ import uk from 'react-intl/locale-data/uk'; import zh from 'react-intl/locale-data/zh'; import bg from 'react-intl/locale-data/bg'; import { localeData as zh_hk } from '../locales/zh-hk'; +import pt_br from '../locales/pt-br'; import getMessagesForLocale from '../locales'; import { hydrateStore } from '../actions/store'; import createStream from '../stream'; @@ -79,6 +80,7 @@ addLocaleData([ ...hu, ...ja, ...pt, + ...pt_br, ...nl, ...no, ...ru, diff --git a/app/assets/javascripts/components/features/compose/components/character_counter.jsx b/app/assets/javascripts/components/features/compose/components/character_counter.jsx index e6b675354..fc64f94a5 100644 --- a/app/assets/javascripts/components/features/compose/components/character_counter.jsx +++ b/app/assets/javascripts/components/features/compose/components/character_counter.jsx @@ -9,14 +9,17 @@ const CharacterCounter = React.createClass({ mixins: [PureRenderMixin], + checkRemainingText (diff) { + if (diff <= 0) { + return <span style={{ fontSize: '16px', cursor: 'default', color: '#ff5050' }}>{diff}</span>; + } + return <span style={{ fontSize: '16px', cursor: 'default' }}>{diff}</span>; + }, + render () { const diff = this.props.max - this.props.text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length; - return ( - <span style={{ fontSize: '16px', cursor: 'default' }}> - {diff} - </span> - ); + return this.checkRemainingText(diff); } }); diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index 0a1dd38ae..73a34ee4f 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -99,7 +99,7 @@ const fr = { "notifications.column_settings.mention": "Mentions :", "notifications.column_settings.reblog": "Partages :", "notifications.clear": "Nettoyer", - "notifications.clear_confirmation": "Voulez-vous vraiment nettoyer toutes vos notifications ?", + "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", "notifications.settings": "Paramètres de la colonne", "privacy.public.short": "Public", "privacy.public.long": "Afficher dans les fils publics", @@ -123,7 +123,25 @@ const fr = { "report.heading": "Nouveau signalement", "report.placeholder": "Commentaires additionnels", "report.submit": "Envoyer", - "report.target": "Signalement" + "report.target": "Signalement", + "onboarding.next": "Suivant", + "onboarding.page_one.welcome": "Bienvenue sur Mastodon !", + "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.", + "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d'utilisateur⋅trice complet est {handle}", + "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.", + "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateurs⋅trices et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu'un qui n'est pas sur cette instance, utilisez son nom d'utilisateur⋅trice complet.", + "onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d'autres préférences.", + "onboarding.page_four.home": "L'Accueil affiche les posts de tou⋅te⋅s les utilisateurs⋅trices que vous suivez", + "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu'un interagit avec vous", + "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateurs⋅trices suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateurs⋅trices de {domain}.", + "onboarding.page_six.almost_done": "Nous y sommes presque...", + "onboarding.page_six.admin": "L'administrateur⋅trice de votre instance est {admin}", + "onboarding.page_six.read_guidelines": "S'il vous plaît, n'oubliez pas de lire les {guidelines} !", + "onboarding.page_six.guidelines": "règles de la communauté", + "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.", + "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant... Bon Appetoot!", + "onboarding.page_six.various_app": "applications mobiles", + "onboarding.skip": "Passer", }; export default fr; diff --git a/app/assets/javascripts/components/locales/hr.jsx b/app/assets/javascripts/components/locales/hr.jsx new file mode 100644 index 000000000..c26e2cc29 --- /dev/null +++ b/app/assets/javascripts/components/locales/hr.jsx @@ -0,0 +1,124 @@ +/** +hrvatski jezik + */ +const hr = { + "account.block": "Blokiraj @{name}", + "account.disclaimer": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.", + "account.edit_profile": "Uredi profil", + "account.follow": "Slijedi", + "account.followers": "Sljedbenici", + "account.follows_you": "te slijedi", + "account.follows": "Slijedi", + "account.mention": "Spomeni @{name}", + "account.mute": "Utišaj @{name}", + "account.posts": "Postovi", + "account.report": "Prijavi @{name}", + "account.requested": "Čeka pristanak", + "account.unblock": "Deblokiraj @{name}", + "account.unfollow": "Prestani slijediti", + "account.unmute": "Poništi utišavanje @{name}", + "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put", + "column_back_button.label": "Natrag", + "column.blocks": "Blokirani korisnici", + "column.community": "Lokalni timeline", + "column.favourites": "Favoriti", + "column.follow_requests": "Zahtjevi za slijeđenje", + "column.home": "Dom", + "column.notifications": "Notifikacije", + "column.public": "Federalni timeline", + "compose_form.placeholder": "Što ti je na umu?", + "compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bit biti boosted ili biti učinjen vidljivim na drugi način neželjenim primateljima.", + "compose_form.publish": "Toot", + "compose_form.sensitive": "Označi media sadržaj kao osjetljiv", + "compose_form.spoiler_placeholder": "Upozorenje o sadržaju", + "compose_form.spoiler": "Sakrij text iza upozorenja", + "emoji_button.label": "Umetni smajlije", + "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!", + "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.", + "empty_column.home.public_timeline": "javni timeline", + "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.", + "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.", + "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio", + "follow_request.authorize": "Authoriziraj", + "follow_request.reject": "Odbij", + "getting_started.apps": "Dostupne su razne aplikacije", + "getting_started.heading": "Počnimo", + "getting_started.open_source_notice": "Mastodon je softver otvorenog koda. Možeš pridonijeti ili prijaviti probleme na GitHubu {github}. {apps}.", + "home.column_settings.advanced": "Napredno", + "home.column_settings.basic": "Osnovno", + "home.column_settings.filter_regex": "Filtriraj s regularnim izrazima", + "home.column_settings.show_reblogs": "Pokaži boosts", + "home.column_settings.show_replies": "Pokaži odgovore", + "home.settings": "Postavke Stupca", + "lightbox.close": "Zatvori", + "loading_indicator.label": "Učitavam...", + "media_gallery.toggle_visible": "Preklopi vidljivost", + "missing_indicator.label": "Nije nađen", + "navigation_bar.blocks": "Blokirani korisnici", + "navigation_bar.community_timeline": "Lokalni timeline", + "navigation_bar.edit_profile": "Uredi profil", + "navigation_bar.favourites": "Favoriti", + "navigation_bar.follow_requests": "Zahtjevi za sljeđenje", + "navigation_bar.info": "Proširena informacija", + "navigation_bar.logout": "Odjavi se", + "navigation_bar.preferences": "Postavke", + "navigation_bar.public_timeline": "Federalni timeline", + "notification.favourite": "{name} je lajkao tvoj status", + "notification.follow": "{name} te sada slijedi", + "notification.reblog": "{name} je boosted tvoj status", + "notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?", + "notifications.clear": "Očisti notifikacije", + "notifications.column_settings.alert": "Desktop notifikacije", + "notifications.column_settings.favourite": "Favoriti:", + "notifications.column_settings.follow": "Novi sljedbenici:", + "notifications.column_settings.mention": "Spominjanja:", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Prikaži u stupcu", + "notifications.column_settings.sound": "Sviraj zvuk", + "notifications.settings": "Postavke rubrike", + "privacy.change": "Podesi status privatnosti", + "privacy.direct.long": "Prikaži samo spomenutim korisnicima", + "privacy.direct.short": "Direktno", + "privacy.private.long": "Prikaži samo sljedbenicima", + "privacy.private.short": "Privatno", + "privacy.public.long": "Postaj na javne timeline", + "privacy.public.short": "Javno", + "privacy.unlisted.long": "Ne prikazuj u javnim timelines", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "Otkaži", + "report.heading": "Nova prijava", + "report.placeholder": "Dodatni komentari", + "report.submit": "Pošalji", + "report.target": "Prijavljivanje", + "search_results.total": "{count} {count, plural, one {result} other {results}}", + "search.placeholder": "Traži", + "search.status_by": "Status od {name}", + "status.delete": "Obriši", + "status.favourite": "Označi omiljenim", + "status.load_more": "Učitaj više", + "status.media_hidden": "Sakriven media sadržaj", + "status.mention": "Spomeni @{name}", + "status.open": "Proširi ovaj status", + "status.reblog": "Boost", + "status.reblogged_by": "{name} boosted", + "status.reply": "Odgovori", + "status.report": "Prijavi @{name}", + "status.sensitive_toggle": "Klikni da bi vidio", + "status.sensitive_warning": "Osjetljiv sadržaj", + "status.show_less": "Pokaži manje", + "status.show_more": "Pokaži više", + "tabs_bar.compose": "Sastavi", + "tabs_bar.federated_timeline": "Federalni", + "tabs_bar.home": "Dom", + "tabs_bar.local_timeline": "Lokalno", + "tabs_bar.notifications": "Notifikacije", + "upload_area.title": "Povuci & spusti kako bi uploadao", + "upload_button.label": "Dodaj media", + "upload_form.undo": "Poništi", + "upload_progress.label": "Uploadam...", + "video_player.toggle_sound": "Toggle zvuk", + "video_player.toggle_visible": "Preklopi vidljivost", + "video_player.expand": "Proširi video", +}; + +export default hr; diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index 7525022b1..7abb315da 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -1,11 +1,13 @@ import en from './en'; import de from './de'; import es from './es'; +import hr from './hr'; import hu from './hu'; import fr from './fr'; import nl from './nl'; import no from './no'; import pt from './pt'; +import pt_br from './pt-br'; import uk from './uk'; import fi from './fi'; import eo from './eo'; @@ -18,11 +20,13 @@ const locales = { en, de, es, + hr, hu, fr, nl, no, pt, + 'pt-BR': pt_br, uk, fi, eo, diff --git a/app/assets/javascripts/components/locales/nl.jsx b/app/assets/javascripts/components/locales/nl.jsx index 8fc3a422f..533bc2aa5 100644 --- a/app/assets/javascripts/components/locales/nl.jsx +++ b/app/assets/javascripts/components/locales/nl.jsx @@ -22,47 +22,69 @@ const nl = { "account.followers": "Volgers", "account.follows_you": "Volgt jou", "account.requested": "Wacht op goedkeuring", + "account.mute": "@{name} negeren", + "account.unmute": "@{name} niet meer negeren", + "account.report": "Report @{name}", "getting_started.heading": "Beginnen", - "getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent, door het e-mailachtige adres in het zoekscherm in te voeren.", - "getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in de statussen wilt vermelden.", + "getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent. Voer hiervoor het e-mailachtige adres in het zoekveld in.", + "getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in toots wilt vermelden.", "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.", - "column.home": "Thuis", + "column.home": "Jouw tijdlijn", "column.community": "Lokale tijdlijn", - "column.public": "Federatietijdlijn", + "column.public": "Globale tijdlijn", "column.notifications": "Meldingen", "tabs_bar.compose": "Schrijven", - "tabs_bar.home": "Thuis", + "tabs_bar.home": "Jouw tijdlijn", "tabs_bar.mentions": "Vermeldingen", - "tabs_bar.public": "Federatietijdlijn", + "tabs_bar.public": "Globale tijdlijn", "tabs_bar.notifications": "Meldingen", "compose_form.placeholder": "Waar ben je mee bezig?", "compose_form.publish": "Toot", "compose_form.sensitive": "Media als gevoelig markeren", "compose_form.spoiler": "Tekst achter waarschuwing verbergen", + "compose_form.spoiler_placeholder": "Waarschuwingstekst", "compose_form.private": "Als privé markeren", - "compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.", + "compose_form.privacy_disclaimer": "Jouw privétoot wordt afgeleverd aan de vermelde gebruikers op {domains}. Vertrouw jij {domainsCount, plural, one {that server} andere {those servers}}? Het privé plaatsen van toots werkt alleen op Mastodon-servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er niet aangegeven dat de toot besloten is, waardoor het kan worden geboost of op een andere manier zichtbaar wordt gemaakt voor mensen waarvoor het niet was bedoeld.", "compose_form.unlisted": "Niet op openbare tijdlijnen tonen", "navigation_bar.edit_profile": "Profiel bewerken", "navigation_bar.preferences": "Voorkeuren", "navigation_bar.community_timeline": "Lokale tijdlijn", - "navigation_bar.public_timeline": "Federatietijdlijn", + "navigation_bar.public_timeline": "Globale tijdlijn", + "navigation_bar.follow_requests": "Volgverzoeken", + "navigation_bar.info": "Uitgebreide informatie", + "navigation_bar.blocks": "Geblokkeerde gebruikers", + "navigation_bar.mutes": "Genegeerde gebruikers", "navigation_bar.logout": "Afmelden", "reply_indicator.cancel": "Annuleren", "search.placeholder": "Zoeken", "search.account": "Account", "search.hashtag": "Hashtag", + "search_results.total": "{count} {count, plural, one {resultaat} other {resultaten}}", "upload_button.label": "Media toevoegen", "upload_form.undo": "Ongedaan maken", - "notification.follow": "{name} volgde jou", - "notification.favourite": "{name} markeerde je status als favoriet", - "notification.reblog": "{name} boostte je status", + "notification.follow": "{name} volgt jou nu", + "notification.favourite": "{name} markeerde jouw toot als favoriet", + "notification.reblog": "{name} boostte jouw toot", "notification.mention": "{name} vermeldde jou", + "notifications.clear_confirmation": "Weet je zeker dat je al jouw meldingen wilt verwijderen?", + "notifications.clear": "Meldingen verwijderen", "notifications.column_settings.alert": "Desktopmeldingen", "notifications.column_settings.show": "In kolom tonen", "notifications.column_settings.follow": "Nieuwe volgers:", "notifications.column_settings.favourite": "Favorieten:", "notifications.column_settings.mention": "Vermeldingen:", "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.sound": "Geluid afspelen", + "notifications.settings": "Kolom-instellingen", + "privacy.change": "Privacy toot aanpassen", + "privacy.direct.long": "Toot alleen naar vermelde gebruikers", + "privacy.direct.short": "Direct", + "privacy.private.long": "Toot alleen naar jouw volgers", + "privacy.private.short": "Privé", + "privacy.public.long": "Toot naar openbare tijdlijnen", + "privacy.public.short": "Openbaar", + "privacy.unlisted.long": "Niet op openbare tijdlijnen weergeven", + "privacy.unlisted.short": "Minder openbaar", }; export default nl; diff --git a/app/assets/javascripts/components/locales/no.jsx b/app/assets/javascripts/components/locales/no.jsx index 43715fb5c..c89c5ede6 100644 --- a/app/assets/javascripts/components/locales/no.jsx +++ b/app/assets/javascripts/components/locales/no.jsx @@ -33,7 +33,7 @@ const no = { "empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!", "empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.", "empty_column.home.public_timeline": "en offentlig tidslinje", - "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.", + "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.", "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.", "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp", "follow_request.authorize": "Autorisér", diff --git a/app/assets/javascripts/components/locales/pt-br.jsx b/app/assets/javascripts/components/locales/pt-br.jsx new file mode 100644 index 000000000..724c5f1ce --- /dev/null +++ b/app/assets/javascripts/components/locales/pt-br.jsx @@ -0,0 +1,125 @@ +const pt_br = { + "account.block": "Bloquear @{name}", + "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.", + "account.edit_profile": "Editar perfil", + "account.follow": "Seguir", + "account.followers": "Seguidores", + "account.follows_you": "É teu seguidor", + "account.follows": "Segue", + "account.mention": "Mencionar @{name}", + "account.mute": "Silenciar @{name}", + "account.posts": "Posts", + "account.report": "Denunciar @{name}", + "account.requested": "A aguardar aprovação", + "account.unblock": "Não bloquear @{name}", + "account.unfollow": "Deixar de seguir", + "account.unmute": "Não silenciar @{name}", + "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", + "column_back_button.label": "Voltar", + "column.blocks": "Utilizadores Bloqueados", + "column.community": "Local", + "column.favourites": "Favoritos", + "column.follow_requests": "Seguidores Pendentes", + "column.home": "Home", + "column.mutes": "Utilizadores silenciados", + "column.notifications": "Notificações", + "column.public": "Global", + "compose_form.placeholder": "Em que estás a pensar?", + "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", + "compose_form.publish": "Publicar", + "compose_form.sensitive": "Marcar media como conteúdo sensível", + "compose_form.spoiler_placeholder": "Aviso de conteúdo", + "compose_form.spoiler": "Esconder texto com aviso", + "emoji_button.label": "Inserir Emoji", + "empty_column.community": "Ainda não existem conteúdo local para mostrar!", + "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag", + "empty_column.home.public_timeline": "global", + "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", + "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", + "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", + "follow_request.authorize": "Autorizar", + "follow_request.reject": "Rejeitar", + "getting_started.apps": "Existem várias aplicações disponíveis", + "getting_started.heading": "Primeiros passos", + "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.", + "home.column_settings.advanced": "Avançado", + "home.column_settings.basic": "Básico", + "home.column_settings.filter_regex": "Filtrar com uma expressão regular", + "home.column_settings.show_reblogs": "Mostrar as partilhas", + "home.column_settings.show_replies": "Mostrar as respostas", + "home.settings": "Parâmetros da listagem Home", + "lightbox.close": "Fechar", + "loading_indicator.label": "Carregando...", + "media_gallery.toggle_visible": "Esconder/Mostrar", + "missing_indicator.label": "Não encontrado", + "navigation_bar.blocks": "Utilizadores bloqueados", + "navigation_bar.community_timeline": "Local", + "navigation_bar.edit_profile": "Editar perfil", + "navigation_bar.favourites": "Favoritos", + "navigation_bar.follow_requests": "Seguidores pendentes", + "navigation_bar.info": "Mais informações", + "navigation_bar.logout": "Sair", + "navigation_bar.mutes": "Utilizadores silenciados", + "navigation_bar.preferences": "Preferências", + "navigation_bar.public_timeline": "Global", + "notification.favourite": "{name} adicionou o teu post aos favoritos", + "notification.follow": "{name} seguiu-te", + "notification.mention": "{name} mencionou-te", + "notification.reblog": "{name} partilhou o teu post", + "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", + "notifications.clear": "Limpar notificações", + "notifications.column_settings.alert": "Notificações no computador", + "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.follow": "Novos seguidores:", + "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.reblog": "Partilhas:", + "notifications.column_settings.show": "Mostrar nas colunas", + "notifications.column_settings.sound": "Reproduzir som", + "notifications.settings": "Parâmetros da listagem de Notificações", + "privacy.change": "Ajustar a privacidade da mensagem", + "privacy.direct.long": "Apenas para utilizadores mencionados", + "privacy.direct.short": "Directo", + "privacy.private.long": "Apenas para os seguidores", + "privacy.private.short": "Privado", + "privacy.public.long": "Publicar em todos os feeds", + "privacy.public.short": "Público", + "privacy.unlisted.long": "Não publicar nos feeds públicos", + "privacy.unlisted.short": "Não listar", + "reply_indicator.cancel": "Cancelar", + "report.heading": "Nova denúncia", + "report.placeholder": "Comentários adicionais", + "report.submit": "Enviar", + "report.target": "Denunciar", + "search_results.total": "{count} {count, plural, one {resultado} other {resultados}}", + "search.placeholder": "Pesquisar", + "search.status_by": "Post de {name}", + "status.delete": "Eliminar", + "status.favourite": "Adicionar aos favoritos", + "status.load_more": "Carregar mais", + "status.media_hidden": "Media escondida", + "status.mention": "Mencionar @{name}", + "status.open": "Expandir", + "status.reblog": "Partilhar", + "status.reblogged_by": "{name} partilhou", + "status.reply": "Responder", + "status.report": "Denúnciar @{name}", + "status.sensitive_toggle": "Clique para ver", + "status.sensitive_warning": "Conteúdo sensível", + "status.show_less": "Mostrar menos", + "status.show_more": "Mostrar mais", + "tabs_bar.compose": "Criar", + "tabs_bar.federated_timeline": "Global", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notificações", + "upload_area.title": "Arraste e solte para enviar", + "upload_button.label": "Adicionar media", + "upload_form.undo": "Anular", + "upload_progress.label": "A gravar...", + "video_player.toggle_sound": "Ligar/Desligar som", + "video_player.toggle_visible": "Ligar/Desligar vídeo", + "video_player.expand": "Expandir vídeo", + "video_player.video_error": "Não é possível ver o vídeo", +}; + +export default pt_br; diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx index cd345a585..88729c94c 100644 --- a/app/assets/javascripts/components/locales/pt.jsx +++ b/app/assets/javascripts/components/locales/pt.jsx @@ -1,128 +1,125 @@ const pt = { - "column_back_button.label": "Voltar", - "lightbox.close": "Fechar", - "loading_indicator.label": "Carregando...", - "status.mention": "Mencionar @{name}", - "status.delete": "Eliminar", - "status.reply": "Responder", - "status.reblog": "Partilhar", - "status.favourite": "Adicionar aos favoritos", - "status.reblogged_by": "{name} partilhou", - "status.sensitive_warning": "Conteúdo sensível", - "status.sensitive_toggle": "Clique para ver", - "status.show_more": "Mostrar mais", - "status.show_less": "Mostrar menos", - "status.open": "Expandir", - "status.report": "Reportar @{name}", - "status.load_more": "Carregar mais", - "status.media_hidden": "Media escondida", - "video_player.toggle_sound": "Ligar/Desligar som", - "video_player.toggle_visible": "Ligar/Desligar vídeo", - "account.mention": "Mencionar @{name}", - "account.edit_profile": "Editar perfil", - "account.unblock": "Não bloquear @{name}", - "account.unfollow": "Não seguir", "account.block": "Bloquear @{name}", - "account.mute": "Mute", - "account.unmute": "Remover Mute", + "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.", + "account.edit_profile": "Editar perfil", "account.follow": "Seguir", - "account.posts": "Posts", - "account.follows": "Segue", "account.followers": "Seguidores", "account.follows_you": "É teu seguidor", + "account.follows": "Segue", + "account.mention": "Mencionar @{name}", + "account.mute": "Silenciar @{name}", + "account.posts": "Posts", + "account.report": "Denunciar @{name}", "account.requested": "A aguardar aprovação", - "account.report": "Denunciar", - "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.", - "getting_started.heading": "Primeiros passos", - "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão colocando um endereço similar a e-mail no campo no topo da barra lateral.", - "getting_started.about_shortcuts": "Se o usuário alvo está no mesmo domínio, só o nome funcionará. A mesma regra se aplica a mencionar pessoas nas postagens.", - "getting_started.about_developer": "Pode seguir o developer deste projecto em Gargron@mastodon.social", - "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.", - "column.home": "Home", - "column.community": "Local", - "column.public": "Global", - "column.notifications": "Notificações", + "account.unblock": "Não bloquear @{name}", + "account.unfollow": "Deixar de seguir", + "account.unmute": "Não silenciar @{name}", + "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", + "column_back_button.label": "Voltar", "column.blocks": "Utilizadores Bloqueados", + "column.community": "Local", "column.favourites": "Favoritos", "column.follow_requests": "Seguidores Pendentes", - "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", - "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", - "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", - "empty_column.home.public_timeline": "global", - "empty_column.community": "Ainda não existem conteúdo local para mostrar!", - "empty_column.hashtag": "Não existe qualquer conteúdo com essa hashtag", - "tabs_bar.compose": "Criar", - "tabs_bar.home": "Home", - "tabs_bar.mentions": "Menções", - "tabs_bar.public": "Público", - "tabs_bar.notifications": "Notificações", - "tabs_bar.local_timeline": "Local", - "tabs_bar.federated_timeline": "Global", + "column.home": "Home", + "column.mutes": "Utilizadores silenciados", + "column.notifications": "Notificações", + "column.public": "Global", "compose_form.placeholder": "Em que estás a pensar?", + "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", "compose_form.publish": "Publicar", "compose_form.sensitive": "Marcar media como conteúdo sensível", + "compose_form.spoiler_placeholder": "Aviso de conteúdo", "compose_form.spoiler": "Esconder texto com aviso", - "compose_form.spoiler_placeholder": "Aviso", - "compose_form.private": "Tornar privado", - "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", - "compose_form.unlisted": "Não mostrar na listagem pública", "emoji_button.label": "Inserir Emoji", - "navigation_bar.edit_profile": "Editar perfil", - "navigation_bar.preferences": "Preferências", - "navigation_bar.community_timeline": "Local", - "navigation_bar.public_timeline": "Global", + "empty_column.community": "Ainda não existem conteúdo local para mostrar!", + "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag", + "empty_column.home.public_timeline": "global", + "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", + "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", + "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", + "follow_request.authorize": "Autorizar", + "follow_request.reject": "Rejeitar", + "getting_started.apps": "Existem várias aplicações disponíveis", + "getting_started.heading": "Primeiros passos", + "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.", + "home.column_settings.advanced": "Avançado", + "home.column_settings.basic": "Básico", + "home.column_settings.filter_regex": "Filtrar com uma expressão regular", + "home.column_settings.show_reblogs": "Mostrar as partilhas", + "home.column_settings.show_replies": "Mostrar as respostas", + "home.settings": "Parâmetros da listagem Home", + "lightbox.close": "Fechar", + "loading_indicator.label": "Carregando...", + "media_gallery.toggle_visible": "Esconder/Mostrar", + "missing_indicator.label": "Não encontrado", "navigation_bar.blocks": "Utilizadores bloqueados", + "navigation_bar.community_timeline": "Local", + "navigation_bar.edit_profile": "Editar perfil", "navigation_bar.favourites": "Favoritos", + "navigation_bar.follow_requests": "Seguidores pendentes", "navigation_bar.info": "Mais informações", "navigation_bar.logout": "Sair", - "navigation_bar.follow_requests": "Seguidores pendentes", - "reply_indicator.cancel": "Cancelar", - "search.placeholder": "Pesquisar", - "search.account": "Conta", - "search.hashtag": "Hashtag", - "search_results.total": "{count} {count, plural, one {resultado} other {resultados}}", - "search.status_by": "Post de {name}", - "upload_button.label": "Adicionar media", - "upload_form.undo": "Anular", - "upload_progress.label": "A gravar…", - "upload_area.title": "Arraste e solte para enviar", - "notification.follow": "{name} seguiu-te", + "navigation_bar.mutes": "Utilizadores silenciados", + "navigation_bar.preferences": "Preferências", + "navigation_bar.public_timeline": "Global", "notification.favourite": "{name} adicionou o teu post aos favoritos", - "notification.reblog": "{name} partilhou o teu post", + "notification.follow": "{name} seguiu-te", "notification.mention": "{name} mencionou-te", + "notification.reblog": "{name} partilhou o teu post", + "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", + "notifications.clear": "Limpar notificações", "notifications.column_settings.alert": "Notificações no computador", - "notifications.column_settings.show": "Mostrar nas colunas", - "notifications.column_settings.sound": "Reproduzir som", - "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.mention": "Menções:", "notifications.column_settings.reblog": "Partilhas:", - "notifications.clear": "Limpar notificações", - "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", - "notifications.settings": "Parâmetros da lista de Notificações", - "privacy.public.short": "Público", + "notifications.column_settings.show": "Mostrar nas colunas", + "notifications.column_settings.sound": "Reproduzir som", + "notifications.settings": "Parâmetros da listagem de Notificações", + "privacy.change": "Ajustar a privacidade da mensagem", + "privacy.direct.long": "Apenas para utilizadores mencionados", + "privacy.direct.short": "Directo", + "privacy.private.long": "Apenas para os seguidores", + "privacy.private.short": "Privado", "privacy.public.long": "Publicar em todos os feeds", - "privacy.unlisted.short": "Não listar", + "privacy.public.short": "Público", "privacy.unlisted.long": "Não publicar nos feeds públicos", - "privacy.private.short": "Privado", - "privacy.private.long": "Apenas para os seguidores", - "privacy.direct.short": "Directo", - "privacy.direct.long": "Apenas para utilizadores mencionados", - "privacy.change": "Ajustar a privacidade da mensagem", - "media_gallery.toggle_visible": "Modificar a visibilidade", - "missing_indicator.label": "Não encontrado", - "follow_request.authorize": "Autorizar", - "follow_request.reject": "Rejeitar", - "home.settings": "Parâmetros da coluna Home", - "home.column_settings.basic": "Básico", - "home.column_settings.show_reblogs": "Mostrar as partilhas", - "home.column_settings.show_replies": "Mostrar as respostas", - "home.column_settings.advanced": "Avançadas", - "home.column_settings.filter_regex": "Filtrar com uma expressão regular", - "report.heading": "Nova denuncia", + "privacy.unlisted.short": "Não listar", + "reply_indicator.cancel": "Cancelar", + "report.heading": "Nova denúncia", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", - "report.target": "Denunciar" + "report.target": "Denunciar", + "search_results.total": "{count} {count, plural, one {resultado} other {resultados}}", + "search.placeholder": "Pesquisar", + "search.status_by": "Post de {name}", + "status.delete": "Eliminar", + "status.favourite": "Adicionar aos favoritos", + "status.load_more": "Carregar mais", + "status.media_hidden": "Media escondida", + "status.mention": "Mencionar @{name}", + "status.open": "Expandir", + "status.reblog": "Partilhar", + "status.reblogged_by": "{name} partilhou", + "status.reply": "Responder", + "status.report": "Denúnciar @{name}", + "status.sensitive_toggle": "Clique para ver", + "status.sensitive_warning": "Conteúdo sensível", + "status.show_less": "Mostrar menos", + "status.show_more": "Mostrar mais", + "tabs_bar.compose": "Criar", + "tabs_bar.federated_timeline": "Global", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notificações", + "upload_area.title": "Arraste e solte para enviar", + "upload_button.label": "Adicionar media", + "upload_form.undo": "Anular", + "upload_progress.label": "A gravar...", + "video_player.toggle_sound": "Ligar/Desligar som", + "video_player.toggle_visible": "Ligar/Desligar vídeo", + "video_player.expand": "Expandir vídeo", + "video_player.video_error": "Não é possível ver o vídeo", }; export default pt; diff --git a/app/assets/javascripts/components/locales/zh-cn.jsx b/app/assets/javascripts/components/locales/zh-cn.jsx new file mode 100644 index 000000000..67baa02a2 --- /dev/null +++ b/app/assets/javascripts/components/locales/zh-cn.jsx @@ -0,0 +1,157 @@ +import zh from 'react-intl/locale-data/zh'; + +const localeData = zh.reduce(function (acc, localeData) { + if (localeData.locale === "zh-Hans-CN") { + // rename the locale "zh-Hans-CN" as "zh-CN" + // (match the code usually used in Accepted-Language header) + acc.push(Object.assign({}, + localeData, + { + "locale": "zh-CN", + "parentLocale": "zh-Hans-CN", + } + )); + } + return acc; +}, []); + +export { localeData as localeData }; + +const zh_cn = { + "account.block": "屏蔽 @{name}", + "account.disclaimer": "由于这个账户处于另一个服务站,实际数字会比这个更多。", + "account.edit_profile": "修改个人资料", + "account.follow": "关注", + "account.followers": "关注的人", + "account.follows_you": "关注你", + "account.follows": "正在关注", + "account.mention": "提及 @{name}", + "account.mute": "将 @{name} 静音", + "account.posts": "嘟文", + "account.report": "举报 @{name}", + "account.requested": "等候审批", + "account.unblock": "解除对 @{name} 的屏蔽", + "account.unfollow": "取消关注", + "account.unmute": "取消 @{name} 的静音", + "boost_modal.combo": "如你想在下次路过时显示,请按{combo},", + "column_back_button.label": "返回", + "column.blocks": "屏蔽用户", + "column.community": "本站时间轴", + // intentional departure from existing "推文" translation for posts: + // "推文" refers to "推特", the official translation for Twitter. + // Currently using a semi-phonetic translation "嘟", which refers + // to train horn sounds, for "toot". + "column.favourites": "赞过的嘟文", + "column.follow_requests": "关注请求", + "column.home": "主页", + "column.notifications": "通知", + "column.public": "跨站公共时间轴", + "compose_form.placeholder": "在想啥?", + "compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任 {domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务站} other {之中有些不是 Mastodon 服务站}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。", + "compose_form.private": "标示为“只有关注你的人能看”", + // Going "toot-toot!" here below. + "compose_form.publish": "嘟嘟!", + "compose_form.sensitive": "将媒体文件标示为“敏感内容”", + "compose_form.spoiler_placeholder": "敏感内容", + "compose_form.spoiler": "将部份文本藏于警告消息之后", + "compose_form.unlisted": "请勿在公共时间轴显示", + "emoji_button.label": "加入表情符号", + "empty_column.community": "本站时间轴暂时未有内容,快贴文来抢头香啊!", + "empty_column.hashtag": "这个标签暂时未有内容。", + "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", + "empty_column.home.public_timeline": "公共时间轴", + "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", + "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。", + "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务站的用户吧!你和本站、友站的交流,将决定这里出现的内容。", + "follow_request.authorize": "批准", + "follow_request.reject": "拒绝", + "getting_started.about_addressing": "只要你知道一位用户的用户名称和域名,你可以用“@用户名称@域名”的格式在搜索栏寻找该用户。", + "getting_started.about_shortcuts": "只要该用户是在你现在的服务站开立,你可以直接输入用户𠱷搜索。同样的规则适用于在嘟文提及别的用户。", + "getting_started.apps": "手机或桌面应用程序", + "getting_started.heading": "开始使用", + "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。你亦可透过{apps}阅读 Mastodon 上的消息。", + "home.column_settings.advanced": "高端", + "home.column_settings.basic": "基本", + "home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤", + "home.column_settings.show_reblogs": "显示被转的嘟文", + "home.column_settings.show_replies": "显示回应嘟文", + "home.settings": "字段设置", + "lightbox.close": "关闭", + "loading_indicator.label": "加载中...", + "media_gallery.toggle_visible": "打开或关上", + "missing_indicator.label": "找不到内容", + "navigation_bar.blocks": "被屏蔽的用户", + "navigation_bar.community_timeline": "本站时间轴", + "navigation_bar.edit_profile": "修改个人资料", + "navigation_bar.favourites": "赞的内容", + "navigation_bar.follow_requests": "关注请求", + "navigation_bar.info": "关于本服务站", + "navigation_bar.logout": "注销", + // intentional departure from https://github.com/tootsuite/mastodon/blob/f864fee1/config/locales/zh-CN.yml#L126: + // clashes for settings/preferences + "navigation_bar.preferences": "首选项", + "navigation_bar.public_timeline": "跨站公共时间轴", + "notification.favourite": "{name} 赞你的嘟文", + "notification.follow": "{name} 开始关注你", + "notification.mention": "{name} 提及你", + "notification.reblog": "{name} 转嘟你的嘟文", + "notifications.clear_confirmation": "你确定要清空通知纪录吗?", + "notifications.clear": "清空通知纪录", + "notifications.column_settings.alert": "显示桌面通知", + "notifications.column_settings.favourite": "赞你的嘟文:", + "notifications.column_settings.follow": "关注你:", + "notifications.column_settings.mention": "提及你:", + "notifications.column_settings.reblog": "转你的嘟文:", + "notifications.column_settings.show": "在通知栏显示", + "notifications.column_settings.sound": "播放音效", + "notifications.settings": "字段设置", + "privacy.change": "调整隐私设置", + "privacy.direct.long": "只有提及的用户能看到", + "privacy.direct.short": "私人消息", + "privacy.private.long": "只有关注你用户能看到", + "privacy.private.short": "关注者", + "privacy.public.long": "在公共时间轴显示", + "privacy.public.short": "公共", + "privacy.unlisted.long": "公开,但不在公共时间轴显示", + "privacy.unlisted.short": "公开", + "reply_indicator.cancel": "取消", + "report.heading": "举报", + "report.placeholder": "额外消息", + "report.submit": "提交", + "report.target": "Reporting", + "search_results.total": "{count} 项结果", + "search.account": "用户", + "search.hashtag": "标签", + "search.placeholder": "搜索", + "search.status_by": "按{name}搜索嘟文", + "status.delete": "删除", + "status.favourite": "赞", + "status.load_more": "加载更多", + "status.media_hidden": "隐藏媒体内容", + "status.mention": "提及 @{name}", + "status.open": "展开嘟文", + "status.reblog": "转嘟", + "status.reblogged_by": "{name} 转嘟", + "status.reply": "回应", + "status.report": "举报 @{name}", + "status.sensitive_toggle": "点击显示", + "status.sensitive_warning": "敏感内容", + "status.show_less": "减少显示", + "status.show_more": "显示更多", + "tabs_bar.compose": "撰写", + "tabs_bar.federated_timeline": "跨站", + "tabs_bar.home": "主页", + "tabs_bar.local_timeline": "本站", + "tabs_bar.mentions": "提及", + "tabs_bar.notifications": "通知", + "tabs_bar.public": "跨站公共时间轴", + "upload_area.title": "将文件拖放至此上传", + "upload_button.label": "上传媒体文件", + "upload_form.undo": "还原", + "upload_progress.label": "上传中……", + "video_player.expand": "展开影片", + "video_player.toggle_sound": "开关音效", + "video_player.toggle_visible": "打开或关上", +}; + +export default zh_cn; diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index e27b88e5f..2916b915b 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -120,10 +120,12 @@ @media screen and (max-width: 600px) { display: block; overflow-y: auto; + -webkit-overflow-scrolling: touch; .sidebar-wrapper, .content-wrapper { flex: 0 0 auto; height: auto; + overflow: initial; } .sidebar { diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 84344e94d..6f407a6d5 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -2050,8 +2050,8 @@ button.icon-button.active i.fa-retweet { .onboarding-modal__pager { height: 80vh; width: 80vw; - max-width: 500px; - max-height: 350px; + max-width: 520px; + max-height: 420px; position: relative; & > div { @@ -2281,6 +2281,7 @@ button.icon-button.active i.fa-retweet { } .boost-modal__container { + overflow-x: scroll; padding: 10px; .status { diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index e5e8697a0..c6a8b5b02 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -42,7 +42,7 @@ code { } } - .input.file, .input.select { + .input.file, .input.select, .input.radio_buttons { padding: 15px 0; margin-bottom: 0; @@ -59,6 +59,15 @@ code { margin-bottom: 25px; } + .input.radio_buttons .radio label { + margin-bottom: 5px; + font-family: inherit; + font-size: 14px; + color: white; + display: block; + width: auto; + } + .input.boolean { margin-bottom: 5px; @@ -72,7 +81,8 @@ code { label.checkbox { position: relative; - padding-left: 25px; + padding-left: 25px; + flex: 1 1 auto; } input[type=checkbox] { @@ -182,6 +192,10 @@ code { } } } + + select { + font-size: 16px; + } } .flash-message { diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 1976ce330..b0e26918e 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -77,9 +77,9 @@ class Api::V1::StatusesController < ApiController end def unreblog - reblog = Status.where(account_id: current_user.account, reblog_of_id: params[:id]).first! - @status = reblog.reblog - @reblogged_map = { @status.id => false } + reblog = Status.where(account_id: current_user.account, reblog_of_id: params[:id]).first! + @status = reblog.reblog + @reblogs_map = { @status.id => false } RemovalWorker.perform_async(reblog.id) @@ -93,7 +93,7 @@ class Api::V1::StatusesController < ApiController def unfavourite @status = Status.find(params[:id]) - @favourited_map = { @status.id => false } + @favourites_map = { @status.id => false } UnfavouriteWorker.perform_async(current_user.account_id, @status.id) diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 57604f1dc..478d21bc7 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -100,17 +100,4 @@ class ApiController < ApplicationController @reblogs_map = Status.reblogs_map(status_ids, current_account) @favourites_map = Status.favourites_map(status_ids, current_account) end - - def set_counters_maps(statuses) # rubocop:disable Style/AccessorMethodName - status_ids = statuses.compact.map { |s| s.reblog? ? s.reblog_of_id : s.id }.uniq - @favourites_counts_map = Favourite.select('status_id, COUNT(id) AS favourites_count').group('status_id').where(status_id: status_ids).map { |f| [f.status_id, f.favourites_count] }.to_h - @reblogs_counts_map = Status.select('statuses.id, COUNT(reblogs.id) AS reblogs_count').joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.id = reblogs.reblog_of_id').where(id: status_ids).group('statuses.id').map { |r| [r.id, r.reblogs_count] }.to_h - end - - def set_account_counters_maps(accounts) # rubocop:disable Style/AccessorMethodName - account_ids = accounts.compact.map(&:id).uniq - @followers_counts_map = Follow.unscoped.select('target_account_id, COUNT(account_id) AS followers_count').group('target_account_id').where(target_account_id: account_ids).map { |f| [f.target_account_id, f.followers_count] }.to_h - @following_counts_map = Follow.unscoped.select('account_id, COUNT(target_account_id) AS following_count').group('account_id').where(account_id: account_ids).map { |f| [f.account_id, f.following_count] }.to_h - @statuses_counts_map = Status.unscoped.select('account_id, COUNT(id) AS statuses_count').group('account_id').where(account_id: account_ids).map { |s| [s.account_id, s.statuses_count] }.to_h - end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index f8050afb5..dd30be32a 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -10,6 +10,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController def build_resource(hash = nil) super(hash) + resource.locale = I18n.locale resource.build_account if resource.account.nil? end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 488c4f944..fa1daf012 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -1,16 +1,19 @@ # frozen_string_literal: true class MediaController < ApplicationController - before_action :set_media_attachment + before_action :verify_permitted_status def show - redirect_to @media_attachment.file.url(:original) + redirect_to media_attachment.file.url(:original) end private - def set_media_attachment - @media_attachment = MediaAttachment.where.not(status_id: nil).find_by!(shortcode: params[:id]) - raise ActiveRecord::RecordNotFound unless @media_attachment.status.permitted?(current_account) + def media_attachment + MediaAttachment.attached.find_by!(shortcode: params[:id]) + end + + def verify_permitted_status + raise ActiveRecord::RecordNotFound unless media_attachment.status.permitted?(current_account) end end diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 1a8ef5f90..4a521d102 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -8,8 +8,13 @@ module WellKnown @magic_key = pem_to_magic_key(@account.keypair.public_key) respond_to do |format| - format.xml { render content_type: 'application/xrd+xml' } - format.json { render content_type: 'application/jrd+json' } + format.any(:json, :html) do + render formats: :json, content_type: 'application/jrd+json' + end + + format.xml do + render content_type: 'application/xrd+xml' + end end rescue ActiveRecord::RecordNotFound head 404 diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb new file mode 100644 index 000000000..a1c3c3521 --- /dev/null +++ b/app/helpers/instance_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module InstanceHelper + def site_title + Setting.site_title.to_s + end + + def site_hostname + Rails.configuration.x.local_domain + end +end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index cf7b9b381..01900b87f 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -7,17 +7,20 @@ module SettingsHelper es: 'Español', eo: 'Esperanto', fr: 'Français', - it: 'Italiano', + hr: 'Hrvatski', hu: 'Magyar', + it: 'Italiano', nl: 'Nederlands', no: 'Norsk', pt: 'Português', + 'pt-BR': 'Português do Brasil', fi: 'Suomi', ru: 'Русский', uk: 'Українська', ja: '日本語', 'zh-CN': '简体中文', 'zh-HK': '繁體中文(香港)', + 'zh-TW': '繁體中文(臺灣)', bg: 'Български', }.freeze diff --git a/app/helpers/site_title_helper.rb b/app/helpers/site_title_helper.rb deleted file mode 100644 index d2caa9203..000000000 --- a/app/helpers/site_title_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module SiteTitleHelper - def site_title - Setting.site_title.to_s - end -end diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb index 4e4031bba..5aeb7b4f9 100644 --- a/app/lib/atom_serializer.rb +++ b/app/lib/atom_serializer.rb @@ -3,13 +3,11 @@ class AtomSerializer include RoutingHelper - INVALID_XML_CHARS = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]/ - class << self def render(element) document = Ox::Document.new(version: '1.0') document << element - ('<?xml version="1.0"?>' + Ox.dump(element)).force_encoding('UTF-8') + ('<?xml version="1.0"?>' + Ox.dump(element, effort: :tolerant)).force_encoding('UTF-8') end end @@ -319,7 +317,7 @@ class AtomSerializer end def sanitize_str(raw_str) - raw_str.to_s.gsub(INVALID_XML_CHARS, '') + raw_str.to_s end def add_namespaces(parent) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 0d9f10a08..e5dbfeeda 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -3,4 +3,5 @@ class ApplicationMailer < ActionMailer::Base default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' } layout 'mailer' + helper :instance end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 64ca92a3a..6abf9c9ca 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -4,6 +4,8 @@ class UserMailer < Devise::Mailer default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' } layout 'mailer' + helper :instance + def confirmation_instructions(user, token, _opts = {}) @resource = user @token = token diff --git a/app/models/account.rb b/app/models/account.rb index 8ceda7f97..259a87451 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -108,10 +108,6 @@ class Account < ApplicationRecord follow_requests.where(target_account: other_account).exists? end - def followers_domains - followers.reorder('').select('DISTINCT accounts.domain').map(&:domain) - end - def local? domain.nil? end @@ -231,18 +227,20 @@ class Account < ApplicationRecord WITH first_degree AS ( SELECT target_account_id FROM follows - WHERE account_id = ? + WHERE account_id = :account_id ) SELECT accounts.* FROM follows INNER JOIN accounts ON follows.target_account_id = accounts.id - WHERE account_id IN (SELECT * FROM first_degree) AND target_account_id NOT IN (SELECT * FROM first_degree) AND target_account_id <> ? + WHERE account_id IN (SELECT * FROM first_degree) AND target_account_id NOT IN (SELECT * FROM first_degree) AND target_account_id <> :account_id GROUP BY target_account_id, accounts.id ORDER BY count(account_id) DESC - LIMIT ? + LIMIT :limit SQL - Account.find_by_sql([sql, account.id, account.id, limit]) + find_by_sql( + [sql, { account_id: account.id, limit: limit }] + ) end def search_for(terms, limit = 10) diff --git a/app/models/block.rb b/app/models/block.rb index ae456a6b6..c978b2200 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -7,4 +7,14 @@ class Block < ApplicationRecord belongs_to :target_account, class_name: 'Account', required: true validates :account_id, uniqueness: { scope: :target_account_id } + + after_create :remove_blocking_cache + after_destroy :remove_blocking_cache + + private + + def remove_blocking_cache + Rails.cache.delete("exclude_account_ids_for:#{account_id}") + Rails.cache.delete("exclude_account_ids_for:#{target_account_id}") + end end diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 41d06e734..32d54476b 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -3,14 +3,14 @@ class Favourite < ApplicationRecord include Paginable - belongs_to :account, inverse_of: :favourites - belongs_to :status, inverse_of: :favourites, counter_cache: true + belongs_to :account, inverse_of: :favourites, required: true + belongs_to :status, inverse_of: :favourites, counter_cache: true, required: true has_one :notification, as: :activity, dependent: :destroy validates :status_id, uniqueness: { scope: :account_id } before_validation do - self.status = status.reblog if status.reblog? + self.status = status.reblog if status&.reblog? end end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 818190214..85e82e12b 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -33,6 +33,7 @@ class MediaAttachment < ApplicationRecord validates :account, presence: true + scope :attached, -> { where.not(status_id: nil) } scope :local, -> { where(remote_url: '') } default_scope { order('id asc') } diff --git a/app/models/mute.rb b/app/models/mute.rb index a5b334c85..d0de62ed5 100644 --- a/app/models/mute.rb +++ b/app/models/mute.rb @@ -3,9 +3,17 @@ class Mute < ApplicationRecord include Paginable - belongs_to :account - belongs_to :target_account, class_name: 'Account' + belongs_to :account, required: true + belongs_to :target_account, class_name: 'Account', required: true - validates :account, :target_account, presence: true validates :account_id, uniqueness: { scope: :target_account_id } + + after_create :remove_blocking_cache + after_destroy :remove_blocking_cache + + private + + def remove_blocking_cache + Rails.cache.delete("exclude_account_ids_for:#{account_id}") + end end diff --git a/app/models/status.rb b/app/models/status.rb index c0a5d9d1b..a9b7327c3 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -10,7 +10,7 @@ class Status < ApplicationRecord belongs_to :application, class_name: 'Doorkeeper::Application' - belongs_to :account, inverse_of: :statuses, counter_cache: true + belongs_to :account, inverse_of: :statuses, counter_cache: true, required: true belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account' belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies @@ -26,7 +26,6 @@ class Status < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy has_one :preview_card, dependent: :destroy - validates :account, presence: true validates :uri, uniqueness: true, unless: 'local?' validates :text, presence: true, unless: 'reblog?' validates_with StatusLengthValidator @@ -184,7 +183,7 @@ class Status < ApplicationRecord private def filter_timeline(query, account) - blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) + Mute.where(account: account).pluck(:target_account_id) + blocked = Rails.cache.fetch("exclude_account_ids_for:#{account.id}") { Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) + Mute.where(account: account).pluck(:target_account_id) } query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? # Only give us statuses from people we haven't blocked, or muted, or that have blocked us query = query.where('accounts.silenced = TRUE') if account.silenced? # and if we're hellbanned, only people who are also hellbanned query diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 497cabb09..63553e9fe 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -4,7 +4,7 @@ class Subscription < ApplicationRecord MIN_EXPIRATION = 3600 * 24 * 7 MAX_EXPIRATION = 3600 * 24 * 30 - belongs_to :account + belongs_to :account, required: true validates :callback_url, presence: true validates :callback_url, uniqueness: { scope: :account_id } diff --git a/app/models/user.rb b/app/models/user.rb index d50101baf..cd1f816ca 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,14 +9,12 @@ class User < ApplicationRecord otp_secret_encryption_key: ENV['OTP_SECRET'], otp_number_of_backup_codes: 10 - belongs_to :account, inverse_of: :user + belongs_to :account, inverse_of: :user, required: true accepts_nested_attributes_for :account - validates :account, presence: true validates :locale, inclusion: I18n.available_locales.map(&:to_s), unless: 'locale.nil?' validates :email, email: true - scope :prolific, -> { joins('inner join statuses on statuses.account_id = users.account_id').select('users.*, count(statuses.id) as statuses_count').group('users.id').order('statuses_count desc') } scope :recent, -> { order('id desc') } scope :admins, -> { where(admin: true) } scope :confirmed, -> { where.not(confirmed_at: nil) } diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index a9cb85500..58a23d978 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -41,7 +41,7 @@ class AccountSearchService < BaseService end def query_username - @_query_username ||= split_query_string.first + @_query_username ||= split_query_string.first || '' end def query_domain diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index ec2b90e07..00af28edd 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -36,7 +36,7 @@ class PostStatusService < BaseService private def validate_media!(media_ids) - return if media_ids.nil? || !media_ids.is_a?(Enumerable) + return if media_ids.blank? || !media_ids.is_a?(Enumerable) raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if media_ids.size > 4 diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index 418c98247..84b29912c 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -1,11 +1,11 @@ - content_for :page_title do - #{Rails.configuration.x.local_domain} + = site_hostname .wrapper.thicc .sidebar-layout .main .panel - %h2= Rails.configuration.x.local_domain + %h2= site_hostname - unless @instance_presenter.site_description.blank? %p= @instance_presenter.site_description.html_safe diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index 39686b531..49ad03557 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -2,13 +2,13 @@ = javascript_include_tag 'application_public', integrity: true - content_for :page_title do - = Rails.configuration.x.local_domain + = site_hostname - content_for :header_tags do %meta{ property: 'og:site_name', content: site_title }/ %meta{ property: 'og:type', content: 'website' }/ - %meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/ - %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.blank? ? t('about.about_mastodon') : @instance_presenter.site_description) }/ + %meta{ property: 'og:title', content: site_hostname }/ + %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon')) }/ %meta{ property: 'og:image', content: asset_url('mastodon_small.jpg') }/ %meta{ property: 'og:image:width', content: '400' }/ %meta{ property: 'og:image:height', content: '400' }/ @@ -72,7 +72,7 @@ = t 'about.features.api' - unless @instance_presenter.site_description.blank? - %h3= t('about.description_headline', domain: Rails.configuration.x.local_domain) + %h3= t('about.description_headline', domain: site_hostname) %p= @instance_presenter.site_description.html_safe .actions diff --git a/app/views/about/terms.en.html.haml b/app/views/about/terms.en.html.haml index e1766ca16..7e0fb94c2 100644 --- a/app/views/about/terms.en.html.haml +++ b/app/views/about/terms.en.html.haml @@ -1,5 +1,5 @@ - content_for :page_title do - #{Rails.configuration.x.local_domain} Terms of Service and Privacy Policy + #{site_hostname} Terms of Service and Privacy Policy .wrapper %h2 Privacy Policy diff --git a/app/views/about/terms.no.html.haml b/app/views/about/terms.no.html.haml index 32ec57ed1..46f62950d 100644 --- a/app/views/about/terms.no.html.haml +++ b/app/views/about/terms.no.html.haml @@ -1,5 +1,5 @@ - content_for :page_title do - #{Rails.configuration.x.local_domain} Personvern og villkår for bruk av nettstedet + #{site_hostname} Personvern og villkår for bruk av nettstedet .wrapper %h2 Personvernserklæring diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 9a70fd16f..b01f3c4e3 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -7,7 +7,7 @@ %meta{ property: 'og:site_name', content: site_title }/ %meta{ property: 'og:type', content: 'profile' }/ - %meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/ + %meta{ property: 'og:title', content: "#{@account.username} on #{site_hostname}" }/ %meta{ property: 'og:description', content: @account.note }/ %meta{ property: 'og:image', content: full_asset_url(@account.avatar.url(:original)) }/ %meta{ property: 'og:image:width', content: '120' }/ @@ -18,7 +18,7 @@ = render partial: 'shared/landing_strip', locals: { account: @account } .h-feed - %data.p-name{ value: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/ + %data.p-name{ value: "#{@account.username} on #{site_hostname}" }/ = render 'header', account: @account diff --git a/app/views/api/oembed/show.json.rabl b/app/views/api/oembed/show.json.rabl index 311c02dad..11dcec538 100644 --- a/app/views/api/oembed/show.json.rabl +++ b/app/views/api/oembed/show.json.rabl @@ -6,7 +6,7 @@ node(:version) { '1.0' } node(:title, &:title) node(:author_name) { |entry| entry.account.display_name.blank? ? entry.account.username : entry.account.display_name } node(:author_url) { |entry| account_url(entry.account) } -node(:provider_name) { Rails.configuration.x.local_domain } +node(:provider_name) { site_hostname } node(:provider_url) { root_url } node(:cache_age) { 86_400 } node(:html) { |entry| "<iframe src=\"#{embed_account_stream_entry_url(entry.account, entry)}\" style=\"width: 100%; overflow: hidden\" frameborder=\"0\" width=\"#{@width}\" height=\"#{@height}\" scrolling=\"no\"></iframe>" } diff --git a/app/views/api/v1/instances/show.rabl b/app/views/api/v1/instances/show.rabl index 88eb08a9e..f5598fde3 100644 --- a/app/views/api/v1/instances/show.rabl +++ b/app/views/api/v1/instances/show.rabl @@ -1,6 +1,6 @@ object false -node(:uri) { Rails.configuration.x.local_domain } +node(:uri) { site_hostname } node(:title) { Setting.site_title } node(:description) { Setting.site_description } node(:email) { Setting.site_contact_email } diff --git a/app/views/api/v1/statuses/_media.rabl b/app/views/api/v1/statuses/_media.rabl index 80d80ea05..2f56c6d07 100644 --- a/app/views/api/v1/statuses/_media.rabl +++ b/app/views/api/v1/statuses/_media.rabl @@ -1,5 +1,5 @@ attributes :id, :remote_url, :type -node(:url) { |media| media.file.blank? ? media.remote_url : full_asset_url(media.file.url(:original)) } -node(:preview_url) { |media| media.file.blank? ? media.remote_url : full_asset_url(media.file.url(:small)) } +node(:url) { |media| full_asset_url(media.file.url(:original)) } +node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } node(:text_url) { |media| media.local? ? medium_url(media) : nil } diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl index a2ab2d060..b599b5cf0 100644 --- a/app/views/home/initial_state.json.rabl +++ b/app/views/home/initial_state.json.rabl @@ -5,7 +5,7 @@ node(:meta) do streaming_api_base_url: @streaming_api_base_url, access_token: @token, locale: I18n.locale, - domain: Rails.configuration.x.local_domain, + domain: site_hostname, me: current_account.id, admin: @admin.try(:id), boost_modal: current_account.user.setting_boost_modal, diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index a27c3de95..688deaebd 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -6,6 +6,7 @@ %meta{'http-equiv' => 'X-UA-Compatible', :content => 'IE=edge'}/ %link{:rel => "apple-touch-icon", :sizes => "180x180", :href => "/apple-touch-icon.png"}/ + %link{:rel => "mask-icon", :href => "/mask-icon.svg", :color => "#2B90D9"}/ %link{:rel => "manifest", :href => "/manifest.json"}/ %meta{:name => "msapplication-config", :content => "/browserconfig.xml"}/ %meta{:name => "theme-color", :content => "#282c37"}/ @@ -13,7 +14,7 @@ %title< - if content_for?(:page_title) - = yield(:page_title) + = yield(:page_title).strip = ' - ' = site_title diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb index 21bf444c3..cdb284de8 100644 --- a/app/views/layouts/mailer.text.erb +++ b/app/views/layouts/mailer.text.erb @@ -1,5 +1,5 @@ <%= yield %> --- -<%= t('application_mailer.signature', instance: Rails.configuration.x.local_domain) %> +<%= t('application_mailer.signature', instance: site_hostname) %> <%= t('application_mailer.settings', link: settings_preferences_url) %> diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index fdde0a681..556102f53 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -4,7 +4,7 @@ - content_for :content do .container= yield .footer - %span.domain= link_to Rails.configuration.x.local_domain, root_path + %span.domain= link_to site_hostname, root_path %span.powered-by = t('generic.powered_by', link: link_to('Mastodon', 'https://github.com/tootsuite/mastodon')).html_safe diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml index 8502913dc..991dd4e94 100644 --- a/app/views/settings/imports/show.html.haml +++ b/app/views/settings/imports/show.html.haml @@ -4,7 +4,7 @@ %p.hint= t('imports.preface') = simple_form_for @import, url: settings_import_path do |f| - = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") } + = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' = f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data') .actions diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 3fdcca041..ce3929629 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -7,7 +7,7 @@ .fields-group = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) } - = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| I18n.t("statuses.visibilities.#{visibility}") }, required: false + = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| I18n.t("statuses.visibilities.#{visibility}") }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' .fields-group = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml index 3536c5ca8..02e694418 100644 --- a/app/views/shared/_landing_strip.html.haml +++ b/app/views/shared/_landing_strip.html.haml @@ -1,5 +1,5 @@ .landing-strip = t('landing_strip_html', name: content_tag(:span, display_name(account), class: :emojify), - domain: Rails.configuration.x.local_domain, + domain: site_hostname, sign_up_path: new_user_registration_path) diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml index dea5e9d40..31efa26c4 100644 --- a/app/views/stream_entries/show.html.haml +++ b/app/views/stream_entries/show.html.haml @@ -4,7 +4,7 @@ %meta{ property: 'og:site_name', content: site_title }/ %meta{ property: 'og:type', content: 'article' }/ - %meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/ + %meta{ property: 'og:title', content: "#{@account.username} on #{site_hostname}" }/ = render 'stream_entries/og_description', activity: @stream_entry.activity = render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account diff --git a/app/views/user_mailer/confirmation_instructions.ja.html.erb b/app/views/user_mailer/confirmation_instructions.ja.html.erb index bbb44b2cc..1232f94b4 100644 --- a/app/views/user_mailer/confirmation_instructions.ja.html.erb +++ b/app/views/user_mailer/confirmation_instructions.ja.html.erb @@ -1,5 +1,11 @@ <p>ようこそ<%= @resource.email %>さん</p> -<p>以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください</p> +<p><%= @instance %>にアカウントが作成されました。</p> + +<p>以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください。</p> <p><%= link_to 'メールアドレスの確認', confirmation_url(@resource, confirmation_token: @token) %></p> + +<p>また、インスタンスの<%= link_to '利用規約', terms_url %>についてもご確認ください。</p> + +<p><%= @instance %> チーム</p> diff --git a/app/views/user_mailer/confirmation_instructions.ja.text.erb b/app/views/user_mailer/confirmation_instructions.ja.text.erb index ad8abee2d..99868ba8a 100644 --- a/app/views/user_mailer/confirmation_instructions.ja.text.erb +++ b/app/views/user_mailer/confirmation_instructions.ja.text.erb @@ -1,5 +1,11 @@ ようこそ<%= @resource.email %>さん -以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください +<%= @instance %>にアカウントが作成されました。 + +以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください。 <%= confirmation_url(@resource, confirmation_token: @token) %> + +また、インスタンスの<%= link_to '利用規約', terms_url %>についてもご確認ください。 + +<%= @instance %> チーム diff --git a/app/views/user_mailer/reset_password_instructions.ja.html.erb b/app/views/user_mailer/reset_password_instructions.ja.html.erb index 156758ef5..d0d7203f4 100644 --- a/app/views/user_mailer/reset_password_instructions.ja.html.erb +++ b/app/views/user_mailer/reset_password_instructions.ja.html.erb @@ -4,5 +4,5 @@ <p><%= link_to 'パスワードを変更', edit_password_url(@resource, reset_password_token: @token) %></p> -<p>このメールに見に覚えのない場合は無視してください。</p> +<p>このメールに身に覚えのない場合は無視してください。</p> <p>上記のリンクにアクセスし、変更をしない限りパスワードは変更されません。</p> diff --git a/app/views/user_mailer/reset_password_instructions.ja.text.erb b/app/views/user_mailer/reset_password_instructions.ja.text.erb index 5fb0eba04..9ed607b58 100644 --- a/app/views/user_mailer/reset_password_instructions.ja.text.erb +++ b/app/views/user_mailer/reset_password_instructions.ja.text.erb @@ -4,5 +4,5 @@ Mastodonアカウントのパスワードの変更がリクエストされまし <%= edit_password_url(@resource, reset_password_token: @token) %> -このメールに見に覚えのない場合は無視してください。 +このメールに身に覚えのない場合は無視してください。 上記のリンクにアクセスし、変更をしない限りパスワードは変更されません。 diff --git a/config/application.rb b/config/application.rb index fa7098b39..396ac33f1 100644 --- a/config/application.rb +++ b/config/application.rb @@ -32,12 +32,14 @@ module Mastodon :es, :fi, :fr, - :it, + :hr, :hu, + :it, :ja, :nl, :no, :pt, + :'pt-BR', :ru, :uk, 'zh-CN', diff --git a/config/environments/production.rb b/config/environments/production.rb index 80021287a..a4cdb2732 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -100,8 +100,10 @@ Rails.application.configure do :address => ENV['SMTP_SERVER'], :user_name => ENV['SMTP_LOGIN'], :password => ENV['SMTP_PASSWORD'], + :domain => ENV['SMTP_DOMAIN'] || ENV['LOCAL_DOMAIN'], - :authentication => ENV['SMTP_AUTH_METHOD'] || :plain, + :authentication => ENV['SMTP_AUTH_METHOD'] == 'none' ? nil : ENV['SMTP_AUTH_METHOD'] || :plain, + :openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'], :enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true, } diff --git a/config/locales/de.yml b/config/locales/de.yml index 75ac4e1bb..dcbeea745 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1,7 +1,7 @@ --- de: about: - about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> soziales Netzwerkserver. Als <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen. + about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> sozialer Netzwerkserver. Als <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen. get_started: Erste Schritte source_code: Quellcode terms: AGB diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml index 4aeb09cd4..c09c15bb4 100644 --- a/config/locales/devise.ja.yml +++ b/config/locales/devise.ja.yml @@ -7,7 +7,7 @@ ja: send_paranoid_instructions: もしあなたのメールアドレスが登録されていれば、まもなくメールアドレスの確認の方法が記載されたメールが送信されます。 failure: already_authenticated: 既にログイン済みです。 - inactive: あなたのアカウントはまだアクティベートされていません。 + inactive: あなたのアカウントはまだ有効化されていません。 invalid: '%{authentication_keys}かパスワードが誤っています' last_attempt: あと1回失敗するとアカウントがロックされます。 locked: アカウントはロックされました。 diff --git a/config/locales/devise.nl.yml b/config/locales/devise.nl.yml index 9057a6775..28e012dfb 100644 --- a/config/locales/devise.nl.yml +++ b/config/locales/devise.nl.yml @@ -3,57 +3,59 @@ nl: devise: confirmations: confirmed: Je account is bevestigd. - send_instructions: Je ontvangt via e-mail instructies hoe je je account kan bevestigen. - send_paranoid_instructions: Als je e-mailadres bestaat in de database, ontvang je via e-mail instructies hoe je je account kan bevestigen. + send_instructions: Je ontvangt via e-mail instructies hoe je jouw account kan bevestigen. + send_paranoid_instructions: Als jouw e-mailadres in de database staat, ontvang je via e-mail instructies hoe je jouw account kan bevestigen. failure: already_authenticated: Je bent al ingelogd. - inactive: Je account is nog niet geactiveerd. - invalid: Ongeldig e-mail of wachtwoord. - invalid_token: Invalide authenticiteit token. - last_attempt: Je hebt nog een poging over voordat je account wordt geblokkeerd. - locked: Je account is gelocked. - not_found_in_database: Ongeldig e-mail of wachtwoord. - timeout: Je sessie is verlopen, log a.u.b. opnieuw in. - unauthenticated: Je dient in te loggen of je in te schrijven. - unconfirmed: Je dient eerst je account te bevestigen. + inactive: Jouw account is nog niet geactiveerd. + invalid: Ongeldig e-mailadres of wachtwoord. + invalid_token: Ongeldige bevestigingscode. + last_attempt: Je hebt nog één poging over voordat jouw account geblokkeerd wordt. + locked: Jouw account is geblokkeerd. + not_found_in_database: Ongeldig e-mailadres of wachtwoord. + timeout: Jouw sessie is verlopen, log opnieuw in. + unauthenticated: Je dient in te loggen of te registreren. + unconfirmed: Je dient eerst jouw account te bevestigen. mailer: confirmation_instructions: - subject: Bevestiging mailadres + subject: 'Mastodon: E-mail bevestigen voor %{instance}' + password_change: + subject: 'Mastodon: Wachtwoord veranderd' reset_password_instructions: - subject: Wachtwoord resetten + subject: 'Mastodon: Wachtwoord opnieuw instellen' unlock_instructions: - subject: Unlock instructies + subject: 'Mastodon: Instructies om account te deblokkeren' omniauth_callbacks: - failure: Kon je niet aanmelden met je %{kind} account, omdat "%{reason}". - success: Successvol aangemeld met je %{kind} account. + failure: Kon je niet aanmelden met jouw %{kind} account, omdat "%{reason}". + success: Successvol aangemeld met jouw %{kind} account. passwords: - no_token: Je kan deze pagina niet benaderen zonder een "wachtwoord reset e-mail" - send_instructions: Je ontvangt via e-mail instructies hoe je je wachtwoord moet resetten. - send_paranoid_instructions: Als je e-mailadres bestaat in de database, ontvang je via e-mail instructies hoe je je wachtwoord moet resetten. - updated: Je wachtwoord is gewijzigd. Je bent nu ingelogd. - updated_not_active: Je wachtwoord is gewijzigd. + no_token: Je kan deze pagina niet benaderen zonder dat je een e-mail om je wachtwoord opnieuw in te stellen hebt ontvangen. + send_instructions: Je ontvangt via e-mail instructies hoe je jouw wachtwoord opnieuw moet instellen. + send_paranoid_instructions: Als jouw e-mailadres in de database staat, ontvang je via e-mail instructies hoe je jouw wachtwoord opnieuw moet instellen. + updated: Jouw wachtwoord is gewijzigd. Je bent nu ingelogd. + updated_not_active: Jouw wachtwoord is gewijzigd. registrations: - destroyed: Je account is verwijderd, wellicht tot ziens! - signed_up: Je bent ingeschreven. - signed_up_but_inactive: Je bent ingeschreven. Je kon alleen niet automatisch ingelogd worden omdat je account nog niet geactiveerd is. - signed_up_but_locked: Je bent ingeschreven. Je kon alleen niet automatisch ingelogd worden omdat je account geblokkeerd is. - signed_up_but_unconfirmed: Je ontvangt via e-mail instructies hoe je je account kunt activeren. - update_needs_confirmation: Je hebt je e-mailadres succesvol gewijzigd, maar we moeten je nieuwe mailadres nog verifiëren. Controleer je e-mail en klik op de link in de mail om je mailadres te verifiëren. - updated: Je account gegevens zijn opgeslagen. + destroyed: Jouw account is verwijderd. Wellicht tot ziens! + signed_up: Je bent geregistreerd. + signed_up_but_inactive: Je bent geregistreerd. Je kon alleen niet automatisch ingelogd worden omdat jouw account nog niet geactiveerd is. + signed_up_but_locked: Je bent ingeschreven. Je kon alleen niet automatisch ingelogd worden omdat jouw account geblokkeerd is. + signed_up_but_unconfirmed: Je ontvangt via e-mail instructies hoe je jouw account kunt activeren. + update_needs_confirmation: Je hebt je e-mailadres succesvol gewijzigd, maar we moeten je nieuwe mailadres nog bevestigen. Controleer jouw e-mail en klik op de link in de mail om jouw e-mailadres te bevestigen. + updated: Jouw accountgegevens zijn opgeslagen. sessions: signed_in: Je bent succesvol ingelogd. signed_out: Je bent succesvol uitgelogd. unlocks: - send_instructions: Je ontvangt via e-mail instructies hoe je je account kan unlocken. - send_paranoid_instructions: Als je e-mailadres bestaat in de database, ontvang je via e-mail instructies hoe je je account kan unlocken. - unlocked: Je account is ge-unlocked. Je kan nu weer inloggen. + send_instructions: Je ontvangt via e-mail instructies hoe je jouw account kan deblokkeren. + send_paranoid_instructions: Als jouw e-mailadres in de database staat, ontvang je via e-mail instructies hoe je jouw account kan deblokkeren. + unlocked: Jouw account is gedeblokkeerd. Je kan nu weer inloggen. errors: messages: already_confirmed: is reeds bevestigd - confirmation_period_expired: moet worden bevestigd binnen %{period}, probeer het a.u.b. nog een keer + confirmation_period_expired: moet worden bevestigd binnen %{period}, probeer het nog een keer expired: is verlopen, vraag een nieuwe aan not_found: niet gevonden - not_locked: is niet gesloten + not_locked: is niet geblokkeerd not_saved: - one: '1 fout blokkeerde het opslaan van deze %{resource}:' - other: "%{count} fouten blokkeerden het opslaan van deze %{resource}:" + one: '1 fout verhinderde het opslaan van deze %{resource}:' + other: "%{count} fouten verhinderden het opslaan van deze %{resource}:" diff --git a/config/locales/devise.pt-BR.yml b/config/locales/devise.pt-BR.yml new file mode 100644 index 000000000..c647fabbd --- /dev/null +++ b/config/locales/devise.pt-BR.yml @@ -0,0 +1,61 @@ +--- +pt-BR: + devise: + confirmations: + confirmed: O seu endereço de email foi confirmado. + send_instructions: Você irá receber um email com instruções em como confirmar o seu endereço de email dentro de alguns minutos. + send_paranoid_instructions: Se o seu endereço de email já existir na nossa base de dados, irá receber um email com instruções em como confirmá-lo dentro de alguns minutos. + failure: + already_authenticated: A sua sessão já está aberta. + inactive: A sua contra ainda não está ativada. + invalid: "%{authentication_keys} ou password inválidos." + last_attempt: Tem mais uma tentativa antes de a sua conta ser protegida. + locked: A sua conta está protegida + not_found_in_database: "%{authentication_keys} ou password inválidos." + timeout: A sua sessão expirou. Por favore entre de novo para continuar. + unauthenticated: Você precsa de entrar ou registar-se antes de continuar. + unconfirmed: Você tem de confirmar o seu endereço de email antes de continuar. + mailer: + confirmation_instructions: + subject: 'Mastodon: Instruções de confirmação' + password_change: + subject: 'Mastodon: Password nova' + reset_password_instructions: + subject: 'Mastodon: Instruções para editar a password' + unlock_instructions: + subject: 'Mastodon: Instruções para desproteger a sua conta' + omniauth_callbacks: + failure: Could not authenticate you from %{kind} because "%{reason}". + success: Successfully authenticated from %{kind} account. + passwords: + no_token: Você não pode aceder a esta página sem ter vindo de um email para mudar a password. Se este for o case, por favor faça questão de verificar que usou o URL no email. + send_instructions: Irá receber um email com instruções em como mudar a sua password dentro de algns minutos. + send_paranoid_instructions: Se seu endereço de email existe na nossa base de dados, irá receber um link para recuperar a sua password dentro de alguns minutos. + updated: A sua password foi alterada. A sua sessão está aberta. + updated_not_active: A sua password foi alterada. + registrations: + destroyed: Adeus! A sua conta foi cancelada. Esperamos vê-lo em breve. + signed_up: Bem vindo! A sua conta foi registada com sucesso. + signed_up_but_inactive: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta ainda não foi ativada. + signed_up_but_locked: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta está protegida. + signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o seu email. Por favor siga o link para ativar a sua conta. + update_needs_confirmation: Você mudou o seu endereço de email ou password, mas é necessário confirmar a mudança. Por favor siga o link que foi enviado para o seu novo endereço de email. + updated: A sua conta foi alterada com sucesso. + sessions: + already_signed_out: Sessão fechada. + signed_in: Sessão iniciada. + signed_out: Sessão fechada. + unlocks: + send_instructions: Irá receber um email com instruções para desproteger a sua conta dentro de alguns minutos. + send_paranoid_instructions: Se a sua conta existe, irá receber um email com instruções a detalhar como a desproteger dentro de alguns minutos. + unlocked: A sua conta foi desprotegida. Por favor inicie sessão para continuar. + errors: + messages: + already_confirmed: já foi confirmado, por favor tente iniciar sessão + confirmation_period_expired: tem de ser confirmado dentro de %{period}, por favor tente outra vez + expired: expirou, por favor tente outra vez + not_found: não encontrado + not_locked: não está protegido + not_saved: + one: '1 erro impediu este %{resource} de ser guardado:' + other: "%{count} erros impediram este %{resource} de ser guardado:" diff --git a/config/locales/devise.pt.yml b/config/locales/devise.pt.yml index 8c049ce8b..dc87cefdd 100644 --- a/config/locales/devise.pt.yml +++ b/config/locales/devise.pt.yml @@ -2,60 +2,60 @@ pt: devise: confirmations: - confirmed: O seu endereço de email foi confirmado. - send_instructions: Você irá receber um email com instruções em como confirmar o seu endereço de email dentro de alguns minutos. - send_paranoid_instructions: Se o seu endereço de email já existir na nossa base de dados, irá receber um email com instruções em como confirmá-lo dentro de alguns minutos. + confirmed: O teu endereço de email foi confirmado. + send_instructions: Vais receber um email com as instruções para confirmar o teu endereço de email dentro de alguns minutos. + send_paranoid_instructions: Se o teu endereço de email já existir na nossa base de dados, vais receber um email com as instruções de confirmação dentro de alguns minutos. failure: - already_authenticated: A sua sessão já está aberta. - inactive: A sua contra ainda não está ativada. - invalid: "%{authentication_keys} ou password inválidos." - last_attempt: Tem mais uma tentativa antes de a sua conta ser protegida. - locked: A sua conta está protegida - not_found_in_database: "%{authentication_keys} ou password inválidos." - timeout: A sua sessão expirou. Por favore entre de novo para continuar. - unauthenticated: Você precsa de entrar ou registar-se antes de continuar. - unconfirmed: Você tem de confirmar o seu endereço de email antes de continuar. + already_authenticated: A tua sessão já está aberta. + inactive: A tua conta ainda não está ativada. + invalid: "%{authentication_keys} ou palavra-passe não válida." + last_attempt: Tens mais uma tentativa antes de a tua conta ficar bloqueada. + locked: A tua conta está bloqueada + not_found_in_database: "%{authentication_keys} ou palavra-passe não válida." + timeout: A tua sessão expirou. Por favor, entra de novo para continuares. + unauthenticated: Precisas de entrar na tua conta ou registares-te antes de continuar. + unconfirmed: Tens de confirmar o teu endereço de email antes de continuar. mailer: confirmation_instructions: - subject: 'Mastodon: Instruções de confirmação' + subject: 'Mastodon: Instruções de confirmação %{instance}' password_change: - subject: 'Mastodon: Password nova' + subject: 'Mastodon: Nova palavra-passe' reset_password_instructions: - subject: 'Mastodon: Instruções para editar a password' + subject: 'Mastodon: Instruções para editar a palavra-passe' unlock_instructions: - subject: 'Mastodon: Instruções para desproteger a sua conta' + subject: 'Mastodon: Instruções para desbloquear a tua conta' omniauth_callbacks: - failure: Could not authenticate you from %{kind} because "%{reason}". - success: Successfully authenticated from %{kind} account. + failure: Não foi possível autenticar %{kind} porque "%{reason}". + success: Autenticado com sucesso na conta %{kind}. passwords: - no_token: Você não pode aceder a esta página sem ter vindo de um email para mudar a password. Se este for o case, por favor faça questão de verificar que usou o URL no email. - send_instructions: Irá receber um email com instruções em como mudar a sua password dentro de algns minutos. - send_paranoid_instructions: Se seu endereço de email existe na nossa base de dados, irá receber um link para recuperar a sua password dentro de alguns minutos. - updated: A sua password foi alterada. A sua sessão está aberta. - updated_not_active: A sua password foi alterada. + no_token: Não pode aceder a esta página se não vier através do link enviado por email para alteração da sua palavra-passe. Se usaste esse link para chegar aqui, por favor verifica que o endereço URL actual é o mesmo do que foi enviado no email. + send_instructions: Vais receber um email com instruções para alterar a palavra-passe dentro de algns minutos. + send_paranoid_instructions: Se o teu endereço de email existe na nossa base de dados, vais receber um link para recuperar a palavra-passe dentro de alguns minutos. + updated: A tua palavra-passe foi alterada. Estás agora autenticado na tua conta. + updated_not_active: A tua palavra-passe foi alterada. registrations: - destroyed: Adeus! A sua conta foi cancelada. Esperamos vê-lo em breve. - signed_up: Bem vindo! A sua conta foi registada com sucesso. - signed_up_but_inactive: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta ainda não foi ativada. - signed_up_but_locked: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta está protegida. - signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o seu email. Por favor siga o link para ativar a sua conta. - update_needs_confirmation: Você mudou o seu endereço de email ou password, mas é necessário confirmar a mudança. Por favor siga o link que foi enviado para o seu novo endereço de email. - updated: A sua conta foi alterada com sucesso. + destroyed: Adeus! A tua conta foi cancelada. Esperamos ver-te em breve. + signed_up: Bem-vindo! A tua conta foi registada com sucesso. + signed_up_but_inactive: A tua conta foi registada. No entanto ainda não está activa. + signed_up_but_locked: A tua conta foi registada. No entanto está bloqueada. + signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o teu email. Por favor segue esse link para activar a tua conta. + update_needs_confirmation: Alteraste o teu endereço de email ou palavra-passe, mas é necessário confirmar essa alteração. Por favor vai ao teu email e segue link que te enviámos. + updated: A tua conta foi actualizada com sucesso. sessions: - already_signed_out: Sessão fechada. + already_signed_out: Sessão encerrada. signed_in: Sessão iniciada. - signed_out: Sessão fechada. + signed_out: Sessão encerrada. unlocks: - send_instructions: Irá receber um email com instruções para desproteger a sua conta dentro de alguns minutos. - send_paranoid_instructions: Se a sua conta existe, irá receber um email com instruções a detalhar como a desproteger dentro de alguns minutos. - unlocked: A sua conta foi desprotegida. Por favor inicie sessão para continuar. + send_instructions: Vais receber um email com instruções para desbloquear a tua conta dentro de alguns minutos. + send_paranoid_instructions: Se a tua conta existe, vais receber um email com instruções a detalhar como a desbloquear dentro de alguns minutos. + unlocked: A sua conta foi desbloqueada. Por favor inica uma nova sessão para continuar. errors: messages: - already_confirmed: já foi confirmado, por favor tente iniciar sessão - confirmation_period_expired: tem de ser confirmado dentro de %{period}, por favor tente outra vez + already_confirmed: já confirmado, por favor tente iniciar sessão + confirmation_period_expired: tem de ser confirmado durante %{period}, por favor tenta outra vez expired: expirou, por favor tente outra vez not_found: não encontrado - not_locked: não está protegido + not_locked: não estava bloqueada not_saved: one: '1 erro impediu este %{resource} de ser guardado:' other: "%{count} erros impediram este %{resource} de ser guardado:" diff --git a/config/locales/doorkeeper.nl.yml b/config/locales/doorkeeper.nl.yml index 91e62dc0c..9edbb8c9f 100644 --- a/config/locales/doorkeeper.nl.yml +++ b/config/locales/doorkeeper.nl.yml @@ -4,7 +4,7 @@ nl: attributes: doorkeeper/application: name: Naam - redirect_uri: Redirect URI + redirect_uri: Redirect-URI scopes: Scopes errors: models: @@ -26,15 +26,15 @@ nl: confirmations: destroy: Weet je het zeker? edit: - title: Bewerk applicatie + title: Applicatie bewerken form: error: Oops! Controleer het formulier op fouten help: native_redirect_uri: Gebruik %{native_redirect_uri} voor lokale tests redirect_uri: 'Gebruik één regel per URI. ' - scopes: Scheid scopes met spaties. Laat leeg om de standaard scopes te gebruiken. + scopes: Scopes met spaties van elkaar scheiden. Laat leeg om de standaardscopes te gebruiken. index: - callback_url: Callback URL + callback_url: Callback-URL name: Naam new: Nieuwe applicatie title: Jouw applicaties @@ -42,8 +42,8 @@ nl: title: Nieuwe applicatie show: actions: Acties - application_id: Applicatie Id - callback_urls: Callback urls + application_id: Applicatie-ID + callback_urls: Callback-URL's scopes: Scopes secret: Secret title: 'Applicatie: %{name}' @@ -58,7 +58,7 @@ nl: prompt: "%{client_name} autoriseren om uw account te gebruiken?" title: Autorisatie vereist show: - title: Autorisatie code + title: Autorisatie-code authorized_applications: buttons: revoke: Intrekken @@ -71,24 +71,24 @@ nl: title: Jouw geautoriseerde applicaties errors: messages: - access_denied: De resource eigenaar of autorisatie-server weigerde het verzoek. - credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured. - invalid_client: Client verificatie is mislukt door onbekende klant, geen client authenticatie opgegeven, of een niet-ondersteunde authenticatie methode. - invalid_grant: De verstrekte autorisatie is ongeldig, verlopen, ingetrokken, komt niet overeen met de redirect uri die is opgegeven, of werd uitgegeven aan een andere klant. - invalid_redirect_uri: De opgegeven redirect uri is niet geldig. - invalid_request: Het verzoek mist een vereiste parameter, bevat een niet-ondersteunde parameter waarde of is anderszins onjuist. - invalid_resource_owner: De verstrekte resource eigenaar gegevens zijn niet geldig of de resource eigenaar kan niet worden gevonden - invalid_scope: De opgevraagde scope is niet geldig, onbekend of onjuist. + access_denied: De resource-eigenaar of autorisatie-server weigerde het verzoek. + credential_flow_not_configured: De wachtwoordgegevens-flow van de resource-eigenaar is mislukt omdat Doorkeeper.configure.resource_owner_from_credentials niet is ingesteld. + invalid_client: Clientverificatie is mislukt door een onbekende client, ontbrekende client-authenticatie of een niet ondersteunde authenticatie-methode. + invalid_grant: De verstrekte autorisatie is ongeldig, verlopen, ingetrokken, komt niet overeen met de redirect-URI die is opgegeven of werd uitgegeven aan een andere client. + invalid_redirect_uri: De opgegeven redirect-URI is ongeldig. + invalid_request: Het verzoek mist een vereiste parameter, bevat een niet ondersteunde parameterwaarde of is anderszins onjuist. + invalid_resource_owner: De verstrekte resource-eigenaargegevens zijn ogeldig of de resource-eigenaar kan niet worden gevonden. + invalid_scope: De opgevraagde scope is ongeldig, onbekend of onjuist. invalid_token: - expired: Het toegangstoken is verlopen - revoked: Het toegangstoken is geweigerd - unknown: Het toegangstoken is ongeldig - resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged. - server_error: De autorisatieserver is een onverwachte voorwaarde tegengekomen die het verzoek verhinderd. + expired: Het toegangssleutel is verlopen + revoked: Het toegangssleutel is geweigerd + unknown: Het toegangssleutel is ongeldig + resource_owner_authenticator_not_configured: Het opzoeken van de resource-eigenaar is mislukt omdat Doorkeeper.configure.resource_owner_authenticator niet is ingesteld. + server_error: De autorisatieserver is is een onverwachte situatie tegengekomen die het verzoek verhinderde. temporarily_unavailable: De autorisatieserver is momenteel niet in staat het verzoek te behandelen als gevolg van een tijdelijke overbelasting of onderhoud aan de server. - unauthorized_client: De client is niet bevoegd om dit verzoek met deze methode uit te voeren. - unsupported_grant_type: Het type autorisatie is niet ondersteund door de autorisatieserver - unsupported_response_type: De autorisatieserver ondersteund dit response type niet + unauthorized_client: De client is niet bevoegd om dit verzoek op deze manier uit te voeren. + unsupported_grant_type: Het type autorisatie wordt niet door de autorisatieserver ondersteund + unsupported_response_type: De autorisatieserver ondersteund dit antwoordtype niet flash: applications: create: @@ -105,10 +105,10 @@ nl: nav: applications: Applicaties home: Home - oauth2_provider: OAuth2 Provider + oauth2_provider: OAuth2-provider application: - title: OAuth autorisatie vereist + title: OAuth-autorisatie vereist scopes: - follow: volg, blokkeer, deblokkeer en stop volgen accounts - read: lees je accountgegevens - write: plaatsen namens jou + follow: volg, blokkeer, deblokkeer en stop het volgen van accounts + read: lees jouw accountgegevens + write: namens jou plaatsen diff --git a/config/locales/doorkeeper.pt-BR.yml b/config/locales/doorkeeper.pt-BR.yml new file mode 100644 index 000000000..85ea3bfcc --- /dev/null +++ b/config/locales/doorkeeper.pt-BR.yml @@ -0,0 +1,112 @@ +--- +pt-BR: + activerecord: + attributes: + doorkeeper/application: + name: Nome + redirect_uri: Redirect URI + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: não pode conter um fragmento. + invalid_uri: tem de ser um URI válido. + relative_uri: tem de ser um URI absoluto. + secured_uri: tem de ser um HTTPS/SSL URI. + doorkeeper: + applications: + buttons: + authorize: Autorizar + cancel: Cancelar + destroy: Destruir + edit: Editar + submit: Submeter + confirmations: + destroy: Tem a certeza? + edit: + title: Editar aplicação + form: + error: Oops! Verifique que o formulário não tem erros + help: + native_redirect_uri: Use %{native_redirect_uri} para testes locais + redirect_uri: Utilize uma linha por URI + scopes: Separate scopes with spaces. Leave blank to use the default scopes. + index: + callback_url: Callback URL + name: Nome + new: Nova Aplicação + title: As suas aplicações + new: + title: Nova aplicação + show: + actions: Ações + application_id: Id de Aplicação + callback_urls: Callback urls + scopes: Scopes + secret: Segredo + title: 'Aplicação: %{name}' + authorizations: + buttons: + authorize: Autorize + deny: Não autorize + error: + title: Ocorreu um erro + new: + able_to: Vai poder + prompt: Aplicação %{client_name} requisita acesso à sua conta + title: Autorização é necessária + show: + title: Código de autorização + authorized_applications: + buttons: + revoke: Revogar + confirmations: + revoke: Tem a certeza? + index: + application: Aplicação + created_at: Criada em + date_format: "%Y-%m-%d %H:%M:%S" + title: As suas aplicações autorizadas + errors: + messages: + access_denied: The resource owner or authorization server denied the request. + credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured. + invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method. + invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. + invalid_redirect_uri: The redirect uri included is not valid. + invalid_request: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. + invalid_resource_owner: The provided resource owner credentials are not valid, or resource owner cannot be found + invalid_scope: The requested scope is invalid, unknown, or malformed. + invalid_token: + expired: O token de acesso expirou + revoked: O token de acesso foi revogado + unknown: O token de acesso é inválido + resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged. + server_error: The authorization server encountered an unexpected condition which prevented it from fulfilling the request. + temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server. + unauthorized_client: The client is not authorized to perform this request using this method. + unsupported_grant_type: The authorization grant type is not supported by the authorization server. + unsupported_response_type: The authorization server does not support this response type. + flash: + applications: + create: + notice: Aplicação criada. + destroy: + notice: Aplicação eliminada. + update: + notice: Aplicação alterada. + authorized_applications: + destroy: + notice: Aplicação revogada. + layouts: + admin: + nav: + applications: Aplicações + oauth2_provider: OAuth2 Provider + application: + title: Autorização OAuth necessária + scopes: + follow: siga, bloqueie, desbloqueie, e deixe de seguir contas + read: tenha acesso aos dados da sua conta + write: públique por si diff --git a/config/locales/doorkeeper.pt.yml b/config/locales/doorkeeper.pt.yml index 2709856e8..87e01ba94 100644 --- a/config/locales/doorkeeper.pt.yml +++ b/config/locales/doorkeeper.pt.yml @@ -23,20 +23,20 @@ pt: edit: Editar submit: Submeter confirmations: - destroy: Tem a certeza? + destroy: Tens a certeza? edit: title: Editar aplicação form: - error: Oops! Verifique que o formulário não tem erros + error: Oops! Verifica que o formulário não tem erros help: - native_redirect_uri: Use %{native_redirect_uri} para testes locais - redirect_uri: Utilize uma linha por URI + native_redirect_uri: Usa %{native_redirect_uri} para testes locais + redirect_uri: Utiliza uma linha por URI scopes: Separate scopes with spaces. Leave blank to use the default scopes. index: callback_url: Callback URL name: Nome new: Nova Aplicação - title: As suas aplicações + title: As tuas aplicações new: title: Nova aplicação show: @@ -54,7 +54,7 @@ pt: title: Ocorreu um erro new: able_to: Vai poder - prompt: Aplicação %{client_name} requisita acesso à sua conta + prompt: Aplicação %{client_name} pede acesso à tua conta title: Autorização é necessária show: title: Código de autorização @@ -62,12 +62,12 @@ pt: buttons: revoke: Revogar confirmations: - revoke: Tem a certeza? + revoke: Tens a certeza? index: application: Aplicação created_at: Criada em date_format: "%Y-%m-%d %H:%M:%S" - title: As suas aplicações autorizadas + title: As tuas aplicações autorizadas errors: messages: access_denied: The resource owner or authorization server denied the request. @@ -107,6 +107,6 @@ pt: application: title: Autorização OAuth necessária scopes: - follow: siga, bloqueie, desbloqueie, e deixe de seguir contas - read: tenha acesso aos dados da sua conta - write: públique por si \ No newline at end of file + follow: siga, bloqueie, desbloqueie, e deixa de seguir contas + read: tenha acesso aos dados da tua conta + write: públique por ti diff --git a/config/locales/fr.yml b/config/locales/fr.yml index ec38b411a..4644d1bad 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -71,6 +71,7 @@ fr: profile_url: URL du profil public: Public push_subscription_expires: Expiration de l'abonnement PuSH + reset_password: Réinitialiser le mot de passe salmon_url: URL Salmon silence: Rendre muet statuses: Statuts @@ -81,6 +82,8 @@ fr: web: Web domain_blocks: add_new: Ajouter + created_msg: Le blocage de domaine est désormais activé + destroyed_msg: Le blocage de domaine a été désactivé domain: Domaine new: create: Créer le blocage @@ -90,8 +93,23 @@ fr: silence: Muet suspend: Suspendre title: Nouveau blocage de domaine + reject_media: Fichiers media rejetés + reject_media_hint: Supprime localement les fichiers media stockés et refuse d'en télécharger ultérieurement. Ne concerne pas les suspensions. + severities: + silence: Rendre muet + suspend: Suspendre severity: Séverité + show: + affected_accounts: + one: Un compte affecté dans la base de données + other: "%{count} comptes affectés dans la base de données" + retroactive: + silence: Annuler le silence sur tous les comptes existants pour ce domaine + suspend: Annuler la suspension sur tous les comptes existants pour ce domaine + title: Annuler le blocage de domaine pour %{domain} + undo: Annuler title: Blocage de domaines + undo: Annuler pubsubhubbub: callback_url: URL de rappel confirmed: Confirmé @@ -192,6 +210,7 @@ fr: blocks: Vous bloquez csv: CSV follows: Vous suivez + mutes: Vous faites taire storage: Médias stockés generic: changes_saved_msg: Les modifications ont été enregistrées avec succès ! @@ -206,8 +225,13 @@ fr: types: blocking: Liste d'utilisateurs⋅trices bloqué⋅es following: Liste d'utilisateurs⋅trices suivi⋅es + muting: Liste d'utilisateurs⋅trices que vous faites taire upload: Importer landing_strip_html: <strong>%{name}</strong> utilise <strong>%{domain}</strong>. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". Si ce n'est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>. + media_attachments: + validations: + images_and_video: Impossible de joindre une vidéo à un statuts contenant déjà des images + too_many: Impossible de joindre plus de 4 fichiers notification_mailer: digest: body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :' @@ -266,10 +290,19 @@ fr: formats: default: "%d %b %Y, %H:%M" two_factor_auth: + code_hint: Entrez le code généré par votre application pour confirmer description_html: Si vous activez <strong>l'identification à deux facteurs</strong>, vous devrez être en possession de votre téléphone afin de générer un code de connexion. disable: Désactiver enable: Activer + enabled_success: Identification à deux facteurs activée avec succès + generate_recovery_codes: Générer les codes de récupération instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone</strong>. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion." + lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés. + manual_instructions: "Si vous ne pouvez pas scanner ce QR code et devez l'entrer manuellement, voici le secret en clair :" + recovery_codes_regenerated: Codes de récupération régénérés avec succès + recovery_instructions: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des code de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants. + setup: Installer + wrong_code: Les codes entrés sont incorrects ! L'heure du serveur et celle de votre appareil sont-elles correctes ? users: invalid_email: L'adresse courriel est invalide invalid_otp_token: Le code d'authentification à deux facteurs est invalide diff --git a/config/locales/ja.yml b/config/locales/ja.yml index c9885b5f6..96fc514da 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -94,7 +94,7 @@ ja: suspend: 停止 title: 新規ドメインブロック reject_media: メディアファイルを拒否 - reject_media_hint: ローカルに保村されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です。 + reject_media_hint: ローカルに保存されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です。 severities: silence: サイレンス suspend: 停止 @@ -272,9 +272,9 @@ ja: over_character_limit: 上限は %{max}文字までです show_more: もっと見る visibilities: - private: Private - フォロワーだけに見せる - public: Public - 全体に公開する - unlisted: Unlisted - トゥートは公開するが、公開タイムラインには表示しない + private: 非公開 - フォロワーだけに公開 + public: 公開 - 公開タイムラインに投稿する + unlisted: 未収載 - トゥートは公開するが、公開タイムラインには表示しない stream_entries: click_to_show: クリックして表示 reblogged: ブーストされました @@ -288,12 +288,12 @@ ja: disable: 無効 enable: 有効 enabled_success: 二段階認証が有効になりました - generate_recovery_codes: 復元コードを生成 + generate_recovery_codes: リカバリーコードを生成 instructions_html: "<strong>Google Authenticatorか、もしくはほかのTOTPアプリでこのQRコードをスキャンしてください。</strong>これ以降、ログインするときはそのアプリで生成されるコードが必要になります。" - lost_recovery_codes: リカバリコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリコードは無効になります。 + lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。 manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:' recovery_codes_regenerated: リカバリーコードが再生成されました。 - recovery_instructions: 携帯電話を紛失した場合、以下の内どれかのリカバリコードを使用してアカウントへアクセスすることができます。 リカバリコードは印刷して安全に保管してください。 + recovery_instructions: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。 リカバリーコードは印刷して安全に保管してください。 setup: 初期設定 wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。 users: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 577b32454..8471743a5 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1,34 +1,34 @@ --- nl: about: - about_mastodon: Mastodon is een <em>vrij, gratis en open-source</em> sociaal netwerk. Een <em>gedecentraliseerd</em> alternatief voor commerciële platforms, het voorkomt de risico's van een enkel bedrijf dat jouw communicatie monopoliseert. Kies een server die je vertrouwt — welke je ook kiest, je kunt met iedere ander communiceren. Iedereen kan een eigen Mastodon-server draaien en naadloos deelnemen in het <em>sociale netwerk</em>. + about_mastodon: Mastodon is een <em>vrij, gratis en open-source</em> sociaal netwerk. Een <em>gedecentraliseerd</em> alternatief voor commerciële platforms. Het voorkomt de risico's van een enkel bedrijf dat jouw communicatie monopoliseert. Kies een server die je vertrouwt — welke je ook kiest, je kunt met elke andere server communiceren. Iedereen kan een eigen Mastodon-server draaien en naadloos deelnemen in het <em>sociale netwerk</em>. about_this: Over deze server apps: Apps - business_email: 'Zakelijk e-mailadres:' + business_email: 'E-mailadres:' closed_registrations: Registreren op deze server is momenteel uitgeschakeld. contact: Contact description_headline: Wat is %{domain}? domain_count_after: andere servers domain_count_before: Verbonden met features: - api: Open API voor apps en services - blocks: Rijke blokkeer- en dempingshulpmiddelen + api: Open API voor apps en diensten + blocks: Uitgebreide blokkeer- en negeerhulpmiddelen characters: 500 tekens per bericht chronology: Tijdlijnen zijn chronologisch - ethics: 'Ethisch design: geen ads, geen spionage' + ethics: 'Ethisch design: geen advertenties, geen spionage' gifv: GIFV-sets en korte video's - privacy: Granulaire privacyinstellingen per bericht + privacy: Nauwkeurige privacyinstellingen per toot (bericht) public: Openbare tijdlijnen features_headline: Wat maakt Mastodon anders get_started: Beginnen links: Links other_instances: Andere servers - source_code: Source code - status_count_after: statussen - status_count_before: Wie schreef + source_code: Broncode + status_count_after: toots + status_count_before: Zij schreven terms: Voorwaarden user_count_after: gebruikers - user_count_before: Thuis naar + user_count_before: Thuisbasis van accounts: follow: Volgen followers: Volgers @@ -37,7 +37,7 @@ nl: people_followed_by: Mensen die %{name} volgt people_who_follow: Mensen die %{name} volgen posts: Berichten - remote_follow: Externe volg + remote_follow: Extern volgen unfollow: Ontvolgen application_mailer: settings: 'E-mailvoorkeuren wijzigen: %{link}' @@ -58,7 +58,7 @@ nl: authorize_follow: error: Helaas, er is een fout opgetreden bij het opzoeken van de externe account follow: Volgen - prompt_html: 'Je (<strong>%{self}</strong>) hebt volgen aangevraagd:' + prompt_html: 'Je (<strong>%{self}</strong>) hebt toestemming gevraagd om iemand te mogen volgen:' title: Volg %{acct} datetime: distance_in_words: @@ -66,84 +66,86 @@ nl: about_x_months: "%{count}ma" about_x_years: "%{count}j" almost_x_years: "%{count}j" - half_a_minute: Net + half_a_minute: Zojuist less_than_x_minutes: "%{count}m" - less_than_x_seconds: Net + less_than_x_seconds: Zojuist over_x_years: "%{count}j" x_days: "%{count}d" x_minutes: "%{count}m" x_months: "%{count}ma" x_seconds: "%{count}s" exports: - blocks: Je blokkeert + blocks: Jij blokkeert csv: CSV - follows: Je volgt + follows: Jij volgt + mutes: Jij negeert storage: Mediaopslag generic: changes_saved_msg: Wijzigingen succesvol opgeslagen! powered_by: mogelijk gemaakt door %{link} - save_changes: Wijziginen opslaan + save_changes: Wijzigingen opslaan validation_errors: one: Er is iets niet helemaal goed! Bekijk onderstaande fout other: Er is iets niet helemaal goed! Bekijk onderstaande %{count} fouten imports: - preface: Je kunt bepaalde gegevens, zoals de mensen die je volgt of blokkeert, importeren voor je account op deze server, als ze zijn geëxporteerd op een andere server. - success: Je gegevens zijn succesvol geüpload en worden binnenkort verwerkt + preface: Je kunt bepaalde gegevens, zoals de mensen die jij volgt of hebt geblokkeerd, naar jouw account op deze server importeren. Je moet deze gegevens wel eerst op de oorspronkelijke server exporteren. + success: Jouw gegevens zijn succesvol geüpload en worden binnenkort verwerkt types: blocking: Blokkeerlijst following: Volglijst + muting: Negeerlijst upload: Uploaden - landing_strip_html: <strong>%{name}</strong> is een gebruiker op <strong>%{domain}</strong>. Je kunt deze volgen of ermee interacteren als je ergens in deze fediverse een account hebt. Als je dat niet hebt, kun je je <a href="%{sign_up_path}">hier aanmelden</a>. + landing_strip_html: <strong>%{name}</strong> is een gebruiker op <strong>%{domain}</strong>. Je kunt deze volgen en ermee communiceren als je ergens in deze fediverse een account hebt. Als je dat niet hebt, kun je je <a href="%{sign_up_path}">hier aanmelden</a>. notification_mailer: digest: - body: 'Hier is een korte samenvatting van wat je hebt gemist op %{instance} sinds je laatste bezoek op %{since}:' - mention: "%{name} vermeldde je in:" + body: 'Hier is een korte samenvatting van wat je hebt gemist op %{instance} sinds jouw laatste bezoek op %{since}:' + mention: "%{name} vermeldde jou in:" new_followers_summary: - one: Je hebt een nieuwe volger! Hoera! - other: Je hebt %{count} nieuwe volgers! Prachtig! + one: Jij hebt een nieuwe volger! Hoera! + other: Jij hebt %{count} nieuwe volgers! Prachtig! subject: - one: "1 nieuwe melding sinds je laatste bezoek \U0001F418" - other: "%{count} nieuwe meldingen sinds je laatste bezoek \U0001F418" + one: "1 nieuwe melding sinds jouw laatste bezoek \U0001F418" + other: "%{count} nieuwe meldingen sinds jouw laatste bezoek \U0001F418" favourite: - body: 'Je status werd door %{name} als favoriet gemarkeerd:' - subject: "%{name} markeerde je status als favoriet" + body: 'Jouw toot werd door %{name} als favoriet gemarkeerd:' + subject: "%{name} markeerde jouw toot als favoriet" follow: - body: "%{name} volgt je nu!" - subject: "%{name} volgt je nu" + body: "%{name} volgt jou nu!" + subject: "%{name} volgt jou nu" follow_request: - body: "%{name} wil je graag volgen" + body: "%{name} wil jou graag volgen" subject: 'Volgen in afwachting: %{name}' mention: - body: 'Je bent door %{name} vermeld in:' - subject: Je bent vermeld door %{name} + body: 'Jij bent door %{name} vermeld in:' + subject: Jij bent vermeld door %{name} reblog: - body: 'Je status werd geboost door %{name}:' - subject: "%{name} booste je status" + body: 'Jouw toot werd door %{name} geboost:' + subject: "%{name} booste jouw toot" pagination: next: Volgende prev: Vorige remote_follow: - acct: Geef je gebruikersnaam@domein op waarvandaan je wilt volgen - missing_resource: Kon vereiste doorverwijzings-URL voor je account niet vinden + acct: Geef jouw account@domein.tld op waarvandaan je wilt volgen + missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden proceed: Ga door om te volgen - prompt: 'Je gaat volgen:' + prompt: 'Jij gaat volgen:' settings: authorized_apps: Geautoriseerde back: Terug naar Mastodon edit_profile: Profiel bewerken - export: Gegevensexport + export: Export import: Import preferences: Voorkeuren settings: Instellingen - two_factor_auth: Twee-factorauthenticatie + two_factor_auth: Tweestapsverificatie statuses: open_in_web: Openen in web - over_character_limit: Tekenlimiet van %{max} overschreden + over_character_limit: Limiet van %{max} tekens overschreden show_more: Toon meer visibilities: private: Alleen aan volgers tonen public: Openbaar - unlisted: Openbaar, maar niet tonen op openbare tijdlijn + unlisted: Openbaar, maar niet op de openbare tijdlijn tonen stream_entries: click_to_show: Klik om te tonen reblogged: boostte @@ -152,10 +154,59 @@ nl: formats: default: "%b %d, %J, %U:%M" two_factor_auth: - description_html: Als je <strong>twee-factorauthenticatie</strong> instelt, kun je alleen aanmelden als je je mobiele telefoon bij je hebt, waarmee je de in te voeren tokens genereert. + description_html: Na het instellen van <strong>tweestapsverificatie</strong>, kun jij je alleen aanmelden als je jouw mobiele telefoon bij je hebt. Hiermee genereer je namelijk de in te voeren aanmeldcode. disable: Uitschakelen enable: Inschakelen - instructions_html: "<strong>Scan deze QR-code in Google Authenticator of een soortgelijke app op je mobiele telefoon</strong>. Van nu af aan creëert deze app tokens die je bij aanmelden moet invoeren." + instructions_html: "<strong>Scan deze QR-code in Google Authenticator of een soortgelijke app op jouw mobiele telefoon</strong>. Van nu af aan genereert deze app aanmeldcodes die je bij het aanmelden moet invoeren." users: - invalid_email: Het e-mailadres is ongeldig - invalid_otp_token: Ongeldige twee-factorcode + invalid_email: E-mailadres is ongeldig + invalid_otp_token: Ongeldige tweestaps-aanmeldcode + errors: + 404: De pagina waarnaar jij op zoek bent bestaat niet. + 410: De pagina waarnaar jij op zoek bent bestaat niet meer. + 422: + title: Veiligheidsverificatie mislukt + content: Veiligheidsverificatie mislukt. Blokkeer je toevallig cookies? + admin.reports: + title: Gerapporteerde toots + status: Toot + unresolved: Onopgelost + resolved: Opgelost + id: ID + target: Target + reported_by: Gerapporteerd door + comment: + label: Opmerking + none: Geen + view: Weergeven + report: 'Gerapporteerde toot #%{id}' + delete: Verwijderen + reported_account: Gerapporteerde account + reported_by: Gerapporteerd door + silence_account: Account stilzwijgen + suspend_account: Account blokkeren + mark_as_resolved: Markeer als opgelost + admin: + settings: + title: Server-instellingen + setting: Instelling + click_to_edit: Klik om te bewerken + contact_information: + label: Contactgegevens + username: Vul een gebruikersnaam in + email: Vul een openbaar gebruikt e-mailadres in + site_title: Naam Mastodon-server + site_description: + title: Omschrijving Mastodon-server + desc_html: "Dit wordt als een alinea op de voorpagina getoond en gebruikt als meta-tag in de paginabron.<br>Je kan HTML gebruiken, zoals <code><a></code> en <code><em></code>." + site_description_extended: + title: Uitgebreide omschrijving Mastodon-server + desc_html: "Wordt op de uitgebreide informatiepagina weergegeven<br>Je kan ook hier HTML gebruiken" + registrations: + open: + title: Open registratie + enabled: Ingeschakeld + disabled: Uitgeschakeld + closed_message: + title: Bericht wanneer registratie is uitgeschakeld + desc_html: "Wordt op de voorpagina weergegeven wanneer registratie van nieuwe accounts is uitgeschakeld<br>En ook hier kan je HTML gebruiken" diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml new file mode 100644 index 000000000..269a1a99b --- /dev/null +++ b/config/locales/pt-BR.yml @@ -0,0 +1,198 @@ +--- +pt-BR: + about: + about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Escolha um servidor que você confie — qualquer um que escolher, você poderá interagir com todo o resto. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas. + about_this: Sobre essa instância + apps: Aplicações + business_email: 'Email comercial:' + closed_registrations: Registros estão fechadas para essa instância. + contact: Contato + description_headline: O que é %{domain}? + domain_count_after: outras instâncias + domain_count_before: Conectado a + features: + api: Aberto para API de aplicações e serviços + blocks: Bloqueos e ferramentas para mudar + characters: 500 caracteres por post + chronology: Timeline são cronologicas + ethics: 'Design ético: sem propaganda, sem tracking' + gifv: GIFV e vídeos curtos + privacy: Granular, privacidade setada por post + public: Timelines públicas + features_headline: O que torna Mastodon diferente + get_started: Comece aqui + links: Links + source_code: Source code + other_instances: Outras instâncias + terms: Termos + user_count_after: usuários + user_count_before: Lugar de + accounts: + follow: Seguir + followers: Seguidores + following: Seguindo + nothing_here: Não há nada aqui! + people_followed_by: Pessoas seguidas por %{name} + people_who_follow: Pessoas que seguem %{name} + posts: Posts + remote_follow: Acesso remoto + unfollow: Unfollow + admin: + accounts: + are_you_sure: Você tem certeza? + display_name: Nome mostrado + domain: Domain + edit: Editar + email: E-mail + feed_url: URL do Feed + followers: Seguidores + follows: Seguindo + location: + all: Todos + local: Local + remote: Remoto + title: Local + media_attachments: Mídia anexadas + moderation: + all: Todos + silenced: Silenciado + suspended: Supenso + title: Moderação + most_recent_activity: Atividade mais recente + most_recent_ip: IP mais recente + not_subscribed: Não inscrito + order: + alphabetic: Alfabética + most_recent: Mais recente + title: Ordem + perform_full_suspension: Fazer suspensão completa + profile_url: URL do perfil + public: Público + push_subscription_expires: PuSH subscription expires + salmon_url: Salmon URL + silence: Silêncio + statuses: Status + title: Contas + undo_silenced: Desfazer silenciar + undo_suspension: Desfazer supensão + username: Usuário + web: Web + domain_blocks: + add_new: Adicionar nova + created_msg: Bloqueio do domínio está sendo processado + destroyed_msg: Bloqueio de domínio está sendo desfeito + domain: Domínio + new: + create: Criar bloqueio + hint: O bloqueio de dominio não vai previnir a criação de entradas no banco de dados, mas irá, retroativamente e automaticamente aplicar métodos de moderação específica nessas contas. + severity: + desc_html: "<strong>Silenciar</strong> irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. <strong>Supender</strong> irá remover todos o conteúdo das contas, mídia e dados do perfil." + silence: Silenciar + suspend: Suspender + title: Novo bloqueio de domínio + reject_media: Rejeitar arquivos de mídia + reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer o download de novos no futuro. Irrelevante em suspensões. + severities: + silence: Silenciar + suspend: Suspender + severity: Severidade + show: + affected_accounts: + one: Uma conta no banco de dados afetada + other: "%{count} contas no banco de dados afetada" + retroactive: + silence: Desilenciar todas as contas existentes nesse domínio + suspend: Desuspender todas as contas existentes nesse domínio + title: Desfazer bloqueio de domínio para %{domain} + title: Bloqueio de domínio + undo: Desfazer + pubsubhubbub: + callback_url: URL de Callback + confirmed: Confirmado + expires_in: Expira em + last_delivery: Última entrega + title: PubSubHubbub + topic: Tópico + reports: + comment: + label: Commentário + none: None + delete: Deletar + id: ID + mark_as_resolved: Marque como resolvido + report: 'Report #%{id}' + reported_account: Conta reportada + reported_by: Reportado por + resolved: Resolvido + silence_account: Conta silenciada + status: Status + suspend_account: Conta suspensa + target: Target + title: Reports + unresolved: Unresolved + view: View + settings: + click_to_edit: Clique para editar + contact_information: + email: Entre um endereço de email público + label: Informação de contato + username: Entre com usuário + registrations: + closed_message: + desc_html: Mostrar na página inicial quando registros estão fecados<br/>Você pode usar tags HTML + title: Mensagem de registro fechados + open: + disabled: Desabilitado + enabled: Habilitado + title: Aberto para registro + setting: Preferências + site_description: + desc_html: Mostrar como parágrafo e usado como meta tag.<br/>Vôce pode usar tags HTML, em particular <code><a></code> e <code><em></code>. + title: Descrição do site + site_description_extended: + desc_html: Mostrar na página de informação extendiada <br/>Você pode usar tags HTML + title: Descrição extendida do site + site_title: Título do site + title: Preferências do site + title: Administração + application_mailer: + settings: 'Mudar preferências de email: %{link}' + signature: notificações Mastodon de %{instance} + view: 'View:' + applications: + invalid_url: URL dada é inválida + auth: + change_password: Mudar senha + didnt_get_confirmation: Não recebeu instruções de confirmação? + forgot_password: Esqueceu a senha? + login: Entrar + register: Registar + resend_confirmation: Reenviar instruções de confirmação + reset_password: Resetar senha + set_new_password: Editar password + generic: + changes_saved_msg: Mudanças guardadas! + powered_by: powered by %{link} + save_changes: Guardar alterações + validation_errors: + one: Algo não está correto. Por favor reveja o erro abaixo + other: Algo não está correto. Por favor reveja os %{count} erros abaixo + notification_mailer: + favourite: + body: 'O seu post foi favoritado por %{name}:' + subject: "%{name} favouritou o seu post" + follow: + body: "%{name} seguiu você!" + subject: "%{name} segue você" + mention: + body: 'Você foi mencionado por %{name} em:' + subject: Foi mencionado por %{name} + reblog: + body: 'O seu post foi reblogado por %{name}:' + subject: "%{name} reblogou o seu post" + pagination: + next: Next + prev: Prev + settings: + edit_profile: Editar perfil + preferences: Preferências diff --git a/config/locales/pt.yml b/config/locales/pt.yml index dc9beae7b..735bc14ba 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -1,64 +1,66 @@ --- pt: about: - about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Escolha um servidor que você confie — qualquer um que escolher, você poderá interagir com todo o resto. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas. - about_this: Sobre essa instância + about_mastodon: Mastodon é uma rede social <em>grátis e em código aberto</em>. Uma alternativa <em>descentralizada</em> às plataformas comerciais, que evita o risco de uma única empresa monopolizar a tua comunicação. Escolhe um servidor que confies, não importa qual, pois vais poder comunicar com todos os outros. Qualquer um pode criar uma instância Mastodon e participar nesta <em>rede social</em>. + about_this: Sobre esta instância apps: Aplicações business_email: 'Email comercial:' - closed_registrations: Registros estão fechadas para essa instância. - contact: Contato - description_headline: O que é %{domain}? + closed_registrations: Novos registos estão fechados nesta instância. + contact: Contacto + description_headline: O que é o %{domain}? domain_count_after: outras instâncias - domain_count_before: Conectado a + domain_count_before: Ligado a features: - api: Aberto para API de aplicações e serviços - blocks: Bloqueos e ferramentas para mudar + api: API aberta para aplicações e serviços + blocks: Ferramentas para silenciar e bloquear characters: 500 caracteres por post - chronology: Timeline são cronologicas - ethics: 'Design ético: sem propaganda, sem tracking' - gifv: GIFV e vídeos curtos - privacy: Granular, privacidade setada por post + chronology: Timelines cronológicas + ethics: 'Design ético: sem públicidade ou tracking' + gifv: GIFV e pequenos vídeos + privacy: Privacidade granular por post public: Timelines públicas features_headline: O que torna Mastodon diferente - get_started: Comece aqui + get_started: Começar links: Links - source_code: Source code other_instances: Outras instâncias + source_code: Código fonte + status_count_after: publicações + status_count_before: Que fizeram terms: Termos - user_count_after: usuários - user_count_before: Lugar de + user_count_after: utilizadores + user_count_before: Casa para accounts: follow: Seguir followers: Seguidores - following: Seguindo + following: A seguir nothing_here: Não há nada aqui! people_followed_by: Pessoas seguidas por %{name} people_who_follow: Pessoas que seguem %{name} posts: Posts - remote_follow: Acesso remoto - unfollow: Unfollow + remote_follow: Seguir remotamente + unfollow: Deixar de seguir admin: accounts: - are_you_sure: Você tem certeza? - display_name: Nome mostrado - domain: Domain + are_you_sure: Tens a certeza? + display_name: Nome a mostrar + domain: Domínio edit: Editar email: E-mail feed_url: URL do Feed followers: Seguidores - follows: Seguindo + follows: A seguir location: all: Todos local: Local remote: Remoto title: Local - media_attachments: Mídia anexadas + media_attachments: Media anexa moderation: all: Todos - silenced: Silenciado - suspended: Supenso + silenced: Silenciados + suspended: Supensos title: Moderação - most_recent_activity: Atividade mais recente + most_recent_activity: Actividade mais recente most_recent_ip: IP mais recente not_subscribed: Não inscrito order: @@ -69,6 +71,7 @@ pt: profile_url: URL do perfil public: Público push_subscription_expires: PuSH subscription expires + reset_password: Reset palavra-passe salmon_url: Salmon URL silence: Silêncio statuses: Status @@ -78,34 +81,35 @@ pt: username: Usuário web: Web domain_blocks: - add_new: Adicionar nova - created_msg: Bloqueio do domínio está sendo processado - destroyed_msg: Bloqueio de domínio está sendo desfeito + add_new: Adicionar novo + created_msg: Bloqueio do domínio está a ser processado + destroyed_msg: Bloqueio de domínio está a ser removido domain: Domínio new: create: Criar bloqueio - hint: O bloqueio de dominio não vai previnir a criação de entradas no banco de dados, mas irá, retroativamente e automaticamente aplicar métodos de moderação específica nessas contas. + hint: O bloqueio de dominio não vai previnir a criação de entradas na base de dados, mas irá retroativamente e automaticamente aplicar métodos de moderação específica nessas contas. severity: - desc_html: "<strong>Silenciar</strong> irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. <strong>Supender</strong> irá remover todos o conteúdo das contas, mídia e dados do perfil." + desc_html: "<strong>Silenciar</strong> irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. <strong>Supender</strong> irá eliminar todo o conteúdo guardado dessa conta, mídia e informação de perfil." silence: Silenciar suspend: Suspender title: Novo bloqueio de domínio - reject_media: Rejeitar arquivos de mídia - reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer o download de novos no futuro. Irrelevante em suspensões. + reject_media: Rejeitar ficheiros de mídia + reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer guardar novos no futuro. Irrelevante na suspensão. severities: silence: Silenciar suspend: Suspender severity: Severidade show: affected_accounts: - one: Uma conta no banco de dados afetada - other: "%{count} contas no banco de dados afetada" + one: Uma conta na base de dados afectada + other: "%{count} contas na base de dados afectadas" retroactive: - silence: Desilenciar todas as contas existentes nesse domínio - suspend: Desuspender todas as contas existentes nesse domínio - title: Desfazer bloqueio de domínio para %{domain} + silence: Não silenciar todas as contas existentes nesse domínio + suspend: Não suspender todas as contas existentes nesse domínio + title: Remover o bloqueio de domínio de %{domain} + undo: Anular title: Bloqueio de domínio - undo: Desfazer + undo: Anular pubsubhubbub: callback_url: URL de Callback confirmed: Confirmado @@ -115,84 +119,84 @@ pt: topic: Tópico reports: comment: - label: Commentário - none: None - delete: Deletar + label: Comentário + none: Nenhum + delete: Eliminar id: ID - mark_as_resolved: Marque como resolvido - report: 'Report #%{id}' - reported_account: Conta reportada - reported_by: Reportado por + mark_as_resolved: Marcar como resolvido + report: 'Denúncia #%{id}' + reported_account: Conta denunciada + reported_by: Denúnciada por resolved: Resolvido silence_account: Conta silenciada - status: Status + status: Estado suspend_account: Conta suspensa target: Target - title: Reports - unresolved: Unresolved - view: View + title: Denúncias + unresolved: Por resolver + view: Ver settings: click_to_edit: Clique para editar contact_information: - email: Entre um endereço de email público - label: Informação de contato - username: Entre com usuário + email: Inserir um endereço de email para tornar público + label: Informação de contacto + username: Insira um nome de utilizador registrations: closed_message: - desc_html: Mostrar na página inicial quando registros estão fecados<br/>Você pode usar tags HTML - title: Mensagem de registro fechados + desc_html: Mostrar na página inicial quando registos estão encerrados<br/>Podes usar tags HTML + title: Mensagem de registos encerrados open: disabled: Desabilitado enabled: Habilitado - title: Aberto para registro + title: Aceitar novos registos setting: Preferências site_description: - desc_html: Mostrar como parágrafo e usado como meta tag.<br/>Vôce pode usar tags HTML, em particular <code><a></code> e <code><em></code>. + desc_html: Mostrar como parágrafo na página inicial e usado como meta tag.<br/>Podes usar tags HTML, em particular <code><a></code> e <code><em></code>. title: Descrição do site site_description_extended: - desc_html: Mostrar na página de informação extendiada <br/>Você pode usar tags HTML - title: Descrição extendida do site + desc_html: Mostrar na página de mais informações<br/>Podes usar tags HTML + title: Página de mais informações site_title: Título do site title: Preferências do site title: Administração application_mailer: - settings: 'Mudar preferências de email: %{link}' - signature: notificações Mastodon de %{instance} - view: 'View:' + settings: 'Alterar preferências de email: %{link}' + signature: notificações Mastodon do %{instance} + view: 'Ver:' applications: - invalid_url: URL dada é inválida + invalid_url: O URL é inválido auth: - change_password: Mudar senha - didnt_get_confirmation: Não recebeu instruções de confirmação? - forgot_password: Esqueceu a senha? + change_password: Alterar palavra-passe + didnt_get_confirmation: Não recebeu o email de confirmação? + forgot_password: Esqueceste a palavra-passe? login: Entrar register: Registar resend_confirmation: Reenviar instruções de confirmação - reset_password: Resetar senha - set_new_password: Editar password + reset_password: Criar nova palavra-passe + set_new_password: Editar palavra-passe generic: - changes_saved_msg: Mudanças guardadas! + changes_saved_msg: Alteraçes guardadas! powered_by: powered by %{link} save_changes: Guardar alterações validation_errors: - one: Algo não está correto. Por favor reveja o erro abaixo - other: Algo não está correto. Por favor reveja os %{count} erros abaixo + one: Algo não está correcto. Por favor vê o erro abaixo + other: Algo não está correto. Por favor vê os %{count} erros abaixo notification_mailer: favourite: - body: 'O seu post foi favoritado por %{name}:' - subject: "%{name} favouritou o seu post" + body: 'O teu post foi adicionado aos favoritos por %{name}:' + subject: "%{name} adicionou o teu post aos favoritos" follow: - body: "%{name} seguiu você!" - subject: "%{name} segue você" + body: "%{name} é teu seguidor!" + subject: "%{name} começou a seguir-te" mention: - body: 'Você foi mencionado por %{name} em:' - subject: Foi mencionado por %{name} + body: 'Foste mencionado por %{name}:' + subject: "%{name} mencionou-te" reblog: - body: 'O seu post foi reblogado por %{name}:' - subject: "%{name} reblogou o seu post" + body: 'O teu post foi partilhado por %{name}:' + subject: "%{name} partilhou o teu post" pagination: - next: Next - prev: Prev + next: Seguinte + prev: Anterior settings: edit_profile: Editar perfil preferences: Preferências diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index 5bc38a87b..c0539fd63 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -6,39 +6,39 @@ nl: avatar: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 120x120px display_name: Maximaal 30 tekens header: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 700x335px - locked: Vereist dat je handmatig volgers accepteert en stelt standaard plaatsen berichten privacy in op alleen-volgers - note: Maximaal 160 characters + locked: Vereist dat je handmatig volgers moet accepteren en stelt de privacy van toots standaard in op alleen volgers + note: Maximaal 160 tekens imports: - data: CSV file geëxporteerd van een andere Mastodon server + data: CSV-bestand dat op een andere Mastodon-server werd geëxporteerd labels: defaults: avatar: Avatar - confirm_new_password: Bevestig nieuw wachtwoord - confirm_password: Bevestig wachtwoord - current_password: Huidige wachtwoord + confirm_new_password: Nieuw wachtwoord bevestigen + confirm_password: Wachtwoord bevestigen + current_password: Huidig wachtwoord data: Gegevens display_name: Weergavenaam email: E-mailadres - header: Kop + header: Omslagfoto locale: Taal locked: Maak account besloten new_password: Nieuwe wachtwoord note: Bio - otp_attempt: Twee-factor code + otp_attempt: Tweestaps-aanmeldcode password: Wachtwoord - setting_default_privacy: Berichten privacy - type: Import type + setting_default_privacy: Tootprivacy + type: Importtype username: gebruikersnaam interactions: - must_be_follower: Blokkeermeldingen van niet-volgers - must_be_following: Blokkeer meldingen van mensen die je niet volgt + must_be_follower: Blokkeermeldingen van mensen die jou niet volgen + must_be_following: Blokkeermeldingen van mensen die jij niet volgt notification_emails: - digest: Verstuur samenvattingse-mails - favourite: Verstuur een e-mail wanneer iemand je status als favoriet markeert - follow: Verstuur een e-mail wanneer iemand je volgt - follow_request: Verstuur een e-mail wanneer iemand je wil volgen - mention: Verstuur een e-mail wanneer iemand je vermeld - reblog: Verstuur een e-mail wanneer iemand je status boost + digest: Verstuur periodiek e-mails met een samenvatting + favourite: Verstuur een e-mail wanneer iemand jouw toot als favoriet markeert + follow: Verstuur een e-mail wanneer iemand jou volgt + follow_request: Verstuur een e-mail wanneer iemand jou wilt volgen + mention: Verstuur een e-mail wanneer iemand jou vermeld + reblog: Verstuur een e-mail wanneer iemand jouw toot heeft geboost 'no': 'Nee' required: mark: "*" diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml new file mode 100644 index 000000000..28f7eeea8 --- /dev/null +++ b/config/locales/simple_form.pt-BR.yml @@ -0,0 +1,30 @@ +--- +pt-BR: + simple_form: + labels: + defaults: + avatar: Avatar + confirm_new_password: Confirme nova senha + confirm_password: Confirme a senha + current_password: Senha atual + display_name: Nome + email: Endereço de email + header: Header + locale: Linguagem + new_password: Nova senha + note: Biografia + password: Senha + username: Usuário + interactions: + must_be_follower: Bloquear notificações de não-seguidores + must_be_following: Bloquear notificações de pessoas que você + notification_emails: + favourite: Enviar email quando alguém favorita um post seu + follow: Enviar email quando alguém seguir você + mention: Enviar email quando alguém mencionar você + reblog: Enviar email quando alguém reblogar um post seu + 'no': 'Não' + required: + mark: "*" + text: necessário + 'yes': 'Sim' diff --git a/config/locales/simple_form.pt.yml b/config/locales/simple_form.pt.yml index e8b5e2d7f..ba3326b23 100644 --- a/config/locales/simple_form.pt.yml +++ b/config/locales/simple_form.pt.yml @@ -1,28 +1,47 @@ --- pt: simple_form: + hints: + defaults: + avatar: PNG, GIF ou JPG. No máximo 2MB. Vai ser reduzido para 120x120px + display_name: No máximo 30 caracteres + header: PNG, GIF or JPG. No máximo 2MB. Vai ser reduzido para 700x335px + locked: Requer que manualmente aproves seguidores e torna o default dos teus posts para privados (apenas seguidores) + note: No máximo 160 caracteres + imports: + data: Ficheiro CSV exportado de outra instância do Mastodon + sessions: + otp: Insere o código o código de autenticação de dois fatores do teu telefone ou utiliza um código de recuperação de acesso. labels: defaults: - avatar: Avatar - confirm_new_password: Confirme nova senha - confirm_password: Confirme a senha - current_password: Senha atual + avatar: Imagem de Perfil + confirm_new_password: Confirme nova palavra-passe + confirm_password: Confirme a palavra-passe + current_password: Palavra-passe actual + data: Data display_name: Nome email: Endereço de email - header: Header - locale: Linguagem - new_password: Nova senha + header: Cabeçalho + locale: Língua + locked: Tornar conta privada + new_password: Nova palavra-passe note: Biografia - password: Senha - username: Usuário + otp_attempt: Código de autenticação de dois fatores + password: Palavra-passe + setting_boost_modal: Pedir confirmação antes de partilhar um post + setting_default_privacy: Privacidade padrão de posts + severity: Severity + type: Import type + username: Utilizador interactions: must_be_follower: Bloquear notificações de não-seguidores - must_be_following: Bloquear notificações de pessoas que você + must_be_following: Bloquear notificações de pessoas que não segues notification_emails: - favourite: Enviar email quando alguém favorita um post seu - follow: Enviar email quando alguém seguir você - mention: Enviar email quando alguém mencionar você - reblog: Enviar email quando alguém reblogar um post seu + digest: Enviar um email da actividade nesta instância + favourite: Enviar email quando alguém adiciona um post teu aos favoritos + follow: Enviar email quando alguém te segue + mention: Enviar email quando alguém te menciona + reblog: Enviar email quando alguém partilhar um post teu 'no': 'Não' required: mark: "*" diff --git a/config/navigation.rb b/config/navigation.rb index 6e2bcd001..7470fea8c 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -19,8 +19,8 @@ SimpleNavigation::Configuration.run do |navigation| admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts} admin.item :pubsubhubbubs, safe_join([fa_icon('paper-plane-o fw'), t('admin.pubsubhubbub.title')]), admin_pubsubhubbub_index_url admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks} - admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url - admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url + admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' } + admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' } admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), admin_settings_url end diff --git a/config/routes.rb b/config/routes.rb index fd186c320..4bb3393b8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,7 +16,7 @@ Rails.application.routes.draw do end get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' } - get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger, defaults: { format: 'json' } + get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger devise_for :users, path: 'auth', controllers: { sessions: 'auth/sessions', diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index a1947ea0e..7dd7b5cd1 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -103,6 +103,12 @@ namespace :mastodon do User.where(id: batch.map(&:id)).delete_all end end + + desc 'List all admin users' + task admins: :environment do + puts 'Admin user emails:' + puts User.admins.map(&:email).join("\n") + end end namespace :settings do @@ -145,8 +151,8 @@ namespace :mastodon do Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account| begin - account.avatar.reprocess! - account.header.reprocess! + account.avatar.reprocess! if account.avatar_content_type == 'image/gif' && !account.avatar.exists?(:static) + account.header.reprocess! if account.header_content_type == 'image/gif' && !account.header.exists?(:static) rescue StandardError => e Rails.logger.error "Error while generating static avatars/headers for account #{account.id}: #{e}" next diff --git a/public/mask-icon.svg b/public/mask-icon.svg new file mode 100644 index 000000000..c35230117 --- /dev/null +++ b/public/mask-icon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" height="16" width="16"><path d="M8 .1A7.9 7.9 0 0 0 .1 8 7.9 7.9 0 0 0 8 15.9 7.9 7.9 0 0 0 15.9 8 7.9 7.9 0 0 0 8 .1zm-.04 4.28h1.7c-.33.23-.44.9-.44 1.25v3.2c0 .7-.55 1.24-1.26 1.24-.7 0-1.26-.55-1.26-1.25v-3.2c0-.68.55-1.24 1.26-1.24zm-4.36.78c.7 0 1.26.55 1.26 1.24v3.2c0 .34.1 1.02.43 1.24H3.6c-.7 0-1.26-.55-1.26-1.24V6.4c0-.7.55-1.24 1.26-1.24zm8.7 0c.7 0 1.26.55 1.26 1.24v3.2c0 .7-.56 1.24-1.27 1.24h-1.7c.32-.22.43-.9.43-1.24V6.4c0-.7.56-1.24 1.26-1.24z" fill="#000"/></svg> \ No newline at end of file diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index 6b26e6693..c2141766e 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -16,9 +16,12 @@ RSpec.describe Auth::RegistrationsController, type: :controller do end describe 'POST #create' do + let(:accept_language) { Rails.application.config.i18n.available_locales.sample.to_s } + before do Setting.open_registrations = true request.env["devise.mapping"] = Devise.mappings[:user] + request.headers["Accept-Language"] = accept_language post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } } end @@ -27,7 +30,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do end it 'creates user' do - expect(User.find_by(email: 'test@example.com')).to_not be_nil + user = User.find_by(email: 'test@example.com') + expect(user).to_not be_nil + expect(user.locale).to eq(accept_language) end end end diff --git a/spec/controllers/media_controller_spec.rb b/spec/controllers/media_controller_spec.rb new file mode 100644 index 000000000..ebf6aa006 --- /dev/null +++ b/spec/controllers/media_controller_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe MediaController do + describe '#show' do + it 'redirects to the file url when attached to a status' do + status = Fabricate(:status) + media_attachment = Fabricate(:media_attachment, status: status) + get :show, params: { id: media_attachment.to_param } + + expect(response).to redirect_to(media_attachment.file.url(:original)) + end + + it 'responds with missing when there is not an attached status' do + media_attachment = Fabricate(:media_attachment, status: nil) + get :show, params: { id: media_attachment.to_param } + + expect(response).to have_http_status(:missing) + end + + it 'raises when shortcode cant be found' do + get :show, params: { id: 'missing' } + + expect(response).to have_http_status(:missing) + end + + it 'raises when not permitted to view' do + status = Fabricate(:status) + media_attachment = Fabricate(:media_attachment, status: status) + allow_any_instance_of(Status).to receive(:permitted?).and_return(false) + get :show, params: { id: media_attachment.to_param } + + expect(response).to have_http_status(:missing) + end + end +end diff --git a/spec/helpers/instance_helper_spec.rb b/spec/helpers/instance_helper_spec.rb new file mode 100644 index 000000000..c42ed6938 --- /dev/null +++ b/spec/helpers/instance_helper_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe InstanceHelper do + describe 'site_title' do + it 'Uses the Setting.site_title value when it exists' do + Setting.site_title = 'New site title' + + expect(helper.site_title).to eq 'New site title' + end + + it 'returns empty string when Setting.site_title is nil' do + Setting.site_title = nil + + expect(helper.site_title).to eq '' + end + end + + describe 'site_hostname' do + around(:each) do |example| + before = Rails.configuration.x.local_domain + example.run + Rails.configuration.x.local_domain = before + end + + it 'returns the local domain value' do + Rails.configuration.x.local_domain = 'example.com' + + expect(helper.site_hostname).to eq 'example.com' + end + end +end diff --git a/spec/helpers/site_title_helper_spec.rb b/spec/helpers/site_title_helper_spec.rb deleted file mode 100644 index 8cfd9cba1..000000000 --- a/spec/helpers/site_title_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "rails_helper" - -describe "site_title" do - it "Uses the Setting.site_title value when it exists" do - Setting.site_title = "New site title" - - expect(helper.site_title).to eq "New site title" - end - - it "returns empty string when Setting.site_title is nil" do - Setting.site_title = nil - - expect(helper.site_title).to eq "" - end -end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb deleted file mode 100644 index 138d25569..000000000 --- a/spec/i18n_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true -require 'i18n/tasks' - -RSpec.describe 'I18n' do - let(:i18n) { I18n::Tasks::BaseTask.new } - let(:missing_keys) { i18n.missing_keys } - let(:unused_keys) { i18n.unused_keys } - - xit 'does not have missing keys' do - expect(missing_keys).to be_empty, "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" - end - - xit 'does not have unused keys' do - expect(unused_keys).to be_empty, "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" - end -end diff --git a/spec/lib/atom_serializer_spec.rb b/spec/lib/atom_serializer_spec.rb new file mode 100644 index 000000000..0009e41a9 --- /dev/null +++ b/spec/lib/atom_serializer_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +RSpec.describe AtomSerializer do + describe '#author' do + it 'returns dumpable XML with emojis' do + account = Fabricate(:account, display_name: '💩') + xml = AtomSerializer.render(AtomSerializer.new.author(account)) + + expect(xml).to be_a String + expect(xml).to match(/<poco:displayName>💩<\/poco:displayName>/) + end + + it 'returns dumpable XML with invalid characters like \b and \v' do + account = Fabricate(:account, display_name: "im l33t\b haxo\b\vr") + xml = AtomSerializer.render(AtomSerializer.new.author(account)) + + expect(xml).to be_a String + expect(xml).to match(/<poco:displayName>im l33t haxor<\/poco:displayName>/) + end + end +end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 46c1ff63c..c39e7986e 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -245,6 +245,23 @@ RSpec.describe Account, type: :model do end end + describe '.triadic_closures' do + it 'finds accounts you dont follow which are followed by accounts you do follow' do + me = Fabricate(:account) + friend = Fabricate(:account) + friends_friend = Fabricate(:account) + me.follow!(friend) + friend.follow!(friends_friend) + + both_follow = Fabricate(:account) + me.follow!(both_follow) + friend.follow!(both_follow) + + results = Account.triadic_closures(me) + expect(results).to eq [friends_friend] + end + end + describe '.find_local' do before do Fabricate(:account, username: 'Alice') diff --git a/spec/requests/webfinger_request_spec.rb b/spec/requests/webfinger_request_spec.rb index b5690d22f..a17d6cc22 100644 --- a/spec/requests/webfinger_request_spec.rb +++ b/spec/requests/webfinger_request_spec.rb @@ -1,33 +1,48 @@ -require "rails_helper" +require 'rails_helper' -describe "The webfinger route" do +describe 'The webfinger route' do let(:alice) { Fabricate(:account, username: 'alice') } - describe "requested without accepts headers" do - it "returns a json response" do - get webfinger_url, params: { resource: alice.to_webfinger_s } + describe 'requested with standard accepts headers' do + it 'returns a json response' do + get webfinger_url(resource: alice.to_webfinger_s) expect(response).to have_http_status(:success) - expect(response.content_type).to eq "application/jrd+json" + expect(response.content_type).to eq 'application/jrd+json' end end - describe "requested with html in accepts headers" do - it "returns a json response" do - headers = { 'HTTP_ACCEPT' => 'text/html' } - get webfinger_url, params: { resource: alice.to_webfinger_s }, headers: headers + describe 'asking for xml format' do + it 'returns an xml response for xml format' do + get webfinger_url(resource: alice.to_webfinger_s, format: :xml) + + expect(response).to have_http_status(:success) + expect(response.content_type).to eq 'application/xrd+xml' + end + + it 'returns an xml response for xml accept header' do + headers = { 'HTTP_ACCEPT' => 'application/xrd+xml' } + get webfinger_url(resource: alice.to_webfinger_s), headers: headers expect(response).to have_http_status(:success) - expect(response.content_type).to eq "application/jrd+json" + expect(response.content_type).to eq 'application/xrd+xml' end end - describe "requested with xml format" do - it "returns an xml response" do - get webfinger_url(resource: alice.to_webfinger_s, format: :xml) + describe 'asking for json format' do + it 'returns a json response for json format' do + get webfinger_url(resource: alice.to_webfinger_s, format: :json) + + expect(response).to have_http_status(:success) + expect(response.content_type).to eq 'application/jrd+json' + end + + it 'returns a json response for json accept header' do + headers = { 'HTTP_ACCEPT' => 'application/jrd+json' } + get webfinger_url(resource: alice.to_webfinger_s), headers: headers expect(response).to have_http_status(:success) - expect(response.content_type).to eq "application/xrd+xml" + expect(response.content_type).to eq 'application/jrd+json' end end end diff --git a/spec/routing/well_known_routes_spec.rb b/spec/routing/well_known_routes_spec.rb index 9540c3de3..2e25605c2 100644 --- a/spec/routing/well_known_routes_spec.rb +++ b/spec/routing/well_known_routes_spec.rb @@ -10,6 +10,6 @@ end describe 'the webfinger route' do it 'routes to correct place with json format' do expect(get('/.well-known/webfinger')). - to route_to('well_known/webfinger#show', format: 'json') + to route_to('well_known/webfinger#show') end end diff --git a/spec/services/account_search_service_spec.rb b/spec/services/account_search_service_spec.rb index fa421c443..723623833 100644 --- a/spec/services/account_search_service_spec.rb +++ b/spec/services/account_search_service_spec.rb @@ -25,6 +25,18 @@ describe AccountSearchService do end describe 'searching local and remote users' do + describe "when only '@'" do + before do + allow(Account).to receive(:find_remote) + allow(Account).to receive(:search_for) + subject.call('@', 10) + end + + it 'uses find_remote with empty query to look for local accounts' do + expect(Account).to have_received(:find_remote).with('', nil) + end + end + describe 'when no domain' do before do allow(Account).to receive(:find_remote) diff --git a/storybook/config.js b/storybook/config.js index 4a111a8b9..924eadf49 100644 --- a/storybook/config.js +++ b/storybook/config.js @@ -17,6 +17,7 @@ window.React = React; function loadStories () { require('./stories/loading_indicator.story.jsx'); require('./stories/button.story.jsx'); + require('./stories/character_counter.story.jsx'); require('./stories/autosuggest_textarea.story.jsx'); } diff --git a/storybook/stories/character_counter.story.jsx b/storybook/stories/character_counter.story.jsx new file mode 100644 index 000000000..931d8a037 --- /dev/null +++ b/storybook/stories/character_counter.story.jsx @@ -0,0 +1,20 @@ +import { storiesOf } from '@kadira/storybook'; +import CharacterCounter from '../../app/assets/javascripts/components/features/compose/components/character_counter'; + +storiesOf('CharacterCounter', module) + .add('no text', () => { + const text = ''; + return <CharacterCounter text={text} max="500" />; + }) + .add('a few strings text', () => { + const text = '0123456789'; + return <CharacterCounter text={text} max="500" />; + }) + .add('the same text', () => { + const text = '01234567890123456789'; + return <CharacterCounter text={text} max="20" />; + }) + .add('over text', () => { + const text = '01234567890123456789012345678901234567890123456789'; + return <CharacterCounter text={text} max="10" />; + }); diff --git a/streaming/index.js b/streaming/index.js index a1e7eaca7..366a39a6a 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -1,3 +1,5 @@ +import os from 'os'; +import cluster from 'cluster'; import dotenv from 'dotenv' import express from 'express' import http from 'http' @@ -14,300 +16,318 @@ dotenv.config({ path: env === 'production' ? '.env.production' : '.env' }) -const pgConfigs = { - development: { - database: 'mastodon_development', - host: '/var/run/postgresql', - max: 10 - }, - - production: { - user: process.env.DB_USER || 'mastodon', - password: process.env.DB_PASS || '', - database: process.env.DB_NAME || 'mastodon_production', - host: process.env.DB_HOST || 'localhost', - port: process.env.DB_PORT || 5432, - max: 10 +if (cluster.isMaster) { + // cluster master + + const core = +process.env.STREAMING_CLUSTER_NUM || (env === 'development' ? 1 : os.cpus().length - 1) + const fork = () => { + const worker = cluster.fork(); + worker.on('exit', (code, signal) => { + log.error(`Worker died with exit code ${code}, signal ${signal} received.`); + setTimeout(() => fork(), 0); + }); + }; + for (let i = 0; i < core; i++) fork(); + log.info(`Starting streaming API server master with ${core} workers`) + +} else { + // cluster worker + + const pgConfigs = { + development: { + database: 'mastodon_development', + host: '/var/run/postgresql', + max: 10 + }, + + production: { + user: process.env.DB_USER || 'mastodon', + password: process.env.DB_PASS || '', + database: process.env.DB_NAME || 'mastodon_production', + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 5432, + max: 10 + } } -} -const app = express() -const pgPool = new pg.Pool(pgConfigs[env]) -const server = http.createServer(app) -const wss = new WebSocket.Server({ server }) - -const redisClient = redis.createClient({ - host: process.env.REDIS_HOST || '127.0.0.1', - port: process.env.REDIS_PORT || 6379, - password: process.env.REDIS_PASSWORD -}) + const app = express() + const pgPool = new pg.Pool(pgConfigs[env]) + const server = http.createServer(app) + const wss = new WebSocket.Server({ server }) -const subs = {} - -redisClient.on('pmessage', (_, channel, message) => { - const callbacks = subs[channel] + const redisClient = redis.createClient({ + host: process.env.REDIS_HOST || '127.0.0.1', + port: process.env.REDIS_PORT || 6379, + password: process.env.REDIS_PASSWORD + }) - log.silly(`New message on channel ${channel}`) + const subs = {} - if (!callbacks) { - return - } + redisClient.on('pmessage', (_, channel, message) => { + const callbacks = subs[channel] - callbacks.forEach(callback => callback(message)) -}) + log.silly(`New message on channel ${channel}`) -redisClient.psubscribe('timeline:*') + if (!callbacks) { + return + } -const subscribe = (channel, callback) => { - log.silly(`Adding listener for ${channel}`) - subs[channel] = subs[channel] || [] - subs[channel].push(callback) -} + callbacks.forEach(callback => callback(message)) + }) -const unsubscribe = (channel, callback) => { - log.silly(`Removing listener for ${channel}`) - subs[channel] = subs[channel].filter(item => item !== callback) -} + redisClient.psubscribe('timeline:*') -const allowCrossDomain = (req, res, next) => { - res.header('Access-Control-Allow-Origin', '*') - res.header('Access-Control-Allow-Headers', 'Authorization, Accept, Cache-Control') - res.header('Access-Control-Allow-Methods', 'GET, OPTIONS') + const subscribe = (channel, callback) => { + log.silly(`Adding listener for ${channel}`) + subs[channel] = subs[channel] || [] + subs[channel].push(callback) + } - next() -} + const unsubscribe = (channel, callback) => { + log.silly(`Removing listener for ${channel}`) + subs[channel] = subs[channel].filter(item => item !== callback) + } -const setRequestId = (req, res, next) => { - req.requestId = uuid.v4() - res.header('X-Request-Id', req.requestId) + const allowCrossDomain = (req, res, next) => { + res.header('Access-Control-Allow-Origin', '*') + res.header('Access-Control-Allow-Headers', 'Authorization, Accept, Cache-Control') + res.header('Access-Control-Allow-Methods', 'GET, OPTIONS') - next() -} + next() + } -const accountFromToken = (token, req, next) => { - pgPool.connect((err, client, done) => { - if (err) { - next(err) - return - } + const setRequestId = (req, res, next) => { + req.requestId = uuid.v4() + res.header('X-Request-Id', req.requestId) - client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 LIMIT 1', [token], (err, result) => { - done() + next() + } + const accountFromToken = (token, req, next) => { + pgPool.connect((err, client, done) => { if (err) { next(err) return } - if (result.rows.length === 0) { - err = new Error('Invalid access token') - err.statusCode = 401 - - next(err) - return - } + client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 LIMIT 1', [token], (err, result) => { + done() - req.accountId = result.rows[0].account_id - - next() - }) - }) -} + if (err) { + next(err) + return + } -const authenticationMiddleware = (req, res, next) => { - if (req.method === 'OPTIONS') { - next() - return - } + if (result.rows.length === 0) { + err = new Error('Invalid access token') + err.statusCode = 401 - const authorization = req.get('Authorization') + next(err) + return + } - if (!authorization) { - const err = new Error('Missing access token') - err.statusCode = 401 + req.accountId = result.rows[0].account_id - next(err) - return + next() + }) + }) } - const token = authorization.replace(/^Bearer /, '') + const authenticationMiddleware = (req, res, next) => { + if (req.method === 'OPTIONS') { + next() + return + } - accountFromToken(token, req, next) -} + const authorization = req.get('Authorization') -const errorMiddleware = (err, req, res, next) => { - log.error(req.requestId, err) - res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ error: err.statusCode ? `${err}` : 'An unexpected error occurred' })) -} + if (!authorization) { + const err = new Error('Missing access token') + err.statusCode = 401 -const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', '); + next(err) + return + } -const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false) => { - log.verbose(req.requestId, `Starting stream from ${id} for ${req.accountId}`) + const token = authorization.replace(/^Bearer /, '') - const listener = message => { - const { event, payload, queued_at } = JSON.parse(message) + accountFromToken(token, req, next) + } - const transmit = () => { - const now = new Date().getTime() - const delta = now - queued_at; + const errorMiddleware = (err, req, res, next) => { + log.error(req.requestId, err) + res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: err.statusCode ? `${err}` : 'An unexpected error occurred' })) + } - log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${payload} Delay: ${delta}ms`) - output(event, payload) - } + const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', '); - // Only messages that may require filtering are statuses, since notifications - // are already personalized and deletes do not matter - if (needsFiltering && event === 'update') { - pgPool.connect((err, client, done) => { - if (err) { - log.error(err) - return - } + const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false) => { + log.verbose(req.requestId, `Starting stream from ${id} for ${req.accountId}`) + + const listener = message => { + const { event, payload, queued_at } = JSON.parse(message) - const unpackedPayload = JSON.parse(payload) - const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)).concat(unpackedPayload.reblog ? [unpackedPayload.reblog.account.id] : []) + const transmit = () => { + const now = new Date().getTime() + const delta = now - queued_at; - client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)}) UNION SELECT target_account_id FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => { - done() + log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${payload} Delay: ${delta}ms`) + output(event, payload) + } + // Only messages that may require filtering are statuses, since notifications + // are already personalized and deletes do not matter + if (needsFiltering && event === 'update') { + pgPool.connect((err, client, done) => { if (err) { log.error(err) return } - if (result.rows.length > 0) { - return - } + const unpackedPayload = JSON.parse(payload) + const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)).concat(unpackedPayload.reblog ? [unpackedPayload.reblog.account.id] : []) + + client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)}) UNION SELECT target_account_id FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => { + done() + + if (err) { + log.error(err) + return + } - transmit() + if (result.rows.length > 0) { + return + } + + transmit() + }) }) - }) - } else { - transmit() + } else { + transmit() + } } + + subscribe(id, listener) + attachCloseHandler(id, listener) } - subscribe(id, listener) - attachCloseHandler(id, listener) -} + // Setup stream output to HTTP + const streamToHttp = (req, res) => { + res.setHeader('Content-Type', 'text/event-stream') + res.setHeader('Transfer-Encoding', 'chunked') -// Setup stream output to HTTP -const streamToHttp = (req, res) => { - res.setHeader('Content-Type', 'text/event-stream') - res.setHeader('Transfer-Encoding', 'chunked') + const heartbeat = setInterval(() => res.write(':thump\n'), 15000) - const heartbeat = setInterval(() => res.write(':thump\n'), 15000) + req.on('close', () => { + log.verbose(req.requestId, `Ending stream for ${req.accountId}`) + clearInterval(heartbeat) + }) - req.on('close', () => { - log.verbose(req.requestId, `Ending stream for ${req.accountId}`) - clearInterval(heartbeat) - }) + return (event, payload) => { + res.write(`event: ${event}\n`) + res.write(`data: ${payload}\n\n`) + } + } - return (event, payload) => { - res.write(`event: ${event}\n`) - res.write(`data: ${payload}\n\n`) + // Setup stream end for HTTP + const streamHttpEnd = req => (id, listener) => { + req.on('close', () => { + unsubscribe(id, listener) + }) } -} -// Setup stream end for HTTP -const streamHttpEnd = req => (id, listener) => { - req.on('close', () => { - unsubscribe(id, listener) - }) -} + // Setup stream output to WebSockets + const streamToWs = (req, ws) => { + const heartbeat = setInterval(() => ws.ping(), 15000) -// Setup stream output to WebSockets -const streamToWs = (req, ws) => { - const heartbeat = setInterval(() => ws.ping(), 15000) + ws.on('close', () => { + log.verbose(req.requestId, `Ending stream for ${req.accountId}`) + clearInterval(heartbeat) + }) - ws.on('close', () => { - log.verbose(req.requestId, `Ending stream for ${req.accountId}`) - clearInterval(heartbeat) - }) + return (event, payload) => { + if (ws.readyState !== ws.OPEN) { + log.error(req.requestId, 'Tried writing to closed socket') + return + } - return (event, payload) => { - if (ws.readyState !== ws.OPEN) { - log.error(req.requestId, 'Tried writing to closed socket') - return + ws.send(JSON.stringify({ event, payload })) } - - ws.send(JSON.stringify({ event, payload })) } -} -// Setup stream end for WebSockets -const streamWsEnd = ws => (id, listener) => { - ws.on('close', () => { - unsubscribe(id, listener) - }) + // Setup stream end for WebSockets + const streamWsEnd = ws => (id, listener) => { + ws.on('close', () => { + unsubscribe(id, listener) + }) - ws.on('error', e => { - unsubscribe(id, listener) - }) -} + ws.on('error', e => { + unsubscribe(id, listener) + }) + } -app.use(setRequestId) -app.use(allowCrossDomain) -app.use(authenticationMiddleware) -app.use(errorMiddleware) + app.use(setRequestId) + app.use(allowCrossDomain) + app.use(authenticationMiddleware) + app.use(errorMiddleware) -app.get('/api/v1/streaming/user', (req, res) => { - streamFrom(`timeline:${req.accountId}`, req, streamToHttp(req, res), streamHttpEnd(req)) -}) + app.get('/api/v1/streaming/user', (req, res) => { + streamFrom(`timeline:${req.accountId}`, req, streamToHttp(req, res), streamHttpEnd(req)) + }) -app.get('/api/v1/streaming/public', (req, res) => { - streamFrom('timeline:public', req, streamToHttp(req, res), streamHttpEnd(req), true) -}) + app.get('/api/v1/streaming/public', (req, res) => { + streamFrom('timeline:public', req, streamToHttp(req, res), streamHttpEnd(req), true) + }) -app.get('/api/v1/streaming/public/local', (req, res) => { - streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true) -}) + app.get('/api/v1/streaming/public/local', (req, res) => { + streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true) + }) -app.get('/api/v1/streaming/hashtag', (req, res) => { - streamFrom(`timeline:hashtag:${req.params.tag}`, req, streamToHttp(req, res), streamHttpEnd(req), true) -}) + app.get('/api/v1/streaming/hashtag', (req, res) => { + streamFrom(`timeline:hashtag:${req.params.tag}`, req, streamToHttp(req, res), streamHttpEnd(req), true) + }) -app.get('/api/v1/streaming/hashtag/local', (req, res) => { - streamFrom(`timeline:hashtag:${req.params.tag}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true) -}) + app.get('/api/v1/streaming/hashtag/local', (req, res) => { + streamFrom(`timeline:hashtag:${req.params.tag}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true) + }) -wss.on('connection', ws => { - const location = url.parse(ws.upgradeReq.url, true) - const token = location.query.access_token - const req = { requestId: uuid.v4() } + wss.on('connection', ws => { + const location = url.parse(ws.upgradeReq.url, true) + const token = location.query.access_token + const req = { requestId: uuid.v4() } - accountFromToken(token, req, err => { - if (err) { - log.error(req.requestId, err) - ws.close() - return - } + accountFromToken(token, req, err => { + if (err) { + log.error(req.requestId, err) + ws.close() + return + } - switch(location.query.stream) { - case 'user': - streamFrom(`timeline:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(ws)) - break; - case 'public': - streamFrom('timeline:public', req, streamToWs(req, ws), streamWsEnd(ws), true) - break; - case 'public:local': - streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(ws), true) - break; - case 'hashtag': - streamFrom(`timeline:hashtag:${location.query.tag}`, req, streamToWs(req, ws), streamWsEnd(ws), true) - break; - case 'hashtag:local': - streamFrom(`timeline:hashtag:${location.query.tag}:local`, req, streamToWs(req, ws), streamWsEnd(ws), true) - break; - default: - ws.close() - } + switch(location.query.stream) { + case 'user': + streamFrom(`timeline:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(ws)) + break; + case 'public': + streamFrom('timeline:public', req, streamToWs(req, ws), streamWsEnd(ws), true) + break; + case 'public:local': + streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(ws), true) + break; + case 'hashtag': + streamFrom(`timeline:hashtag:${location.query.tag}`, req, streamToWs(req, ws), streamWsEnd(ws), true) + break; + case 'hashtag:local': + streamFrom(`timeline:hashtag:${location.query.tag}:local`, req, streamToWs(req, ws), streamWsEnd(ws), true) + break; + default: + ws.close() + } + }) }) -}) -server.listen(process.env.PORT || 4000, () => { - log.level = process.env.LOG_LEVEL || 'verbose' - log.info(`Starting streaming API server on port ${server.address().port}`) -}) + server.listen(process.env.PORT || 4000, () => { + log.level = process.env.LOG_LEVEL || 'verbose' + log.info(`Starting streaming API server worker on port ${server.address().port}`) + }) +} |