From 0022286b46770260948d4a2f2c69cee3a9aef344 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sun, 29 Nov 2020 14:13:27 -0800 Subject: [PATCH] fix: first stab at i18n, extract English strings, add French (#1904) * first attempt * progress * working * working * test timeago * rm * get timeago working * reduce size * fix whitespace * more intl stuff * more effort * more work * more progress * more work * more intl * set lang=LOCALE * flatten * more work * add ltr/rtl * more work * add comments * yet more work * still more work * more work * fix tests * more test and string fixes * fix test * fix test * fix test * fix some more strings, add test * fix snackbar * fix } * fix typo * fix english * measure perf * start on french * more work on french * more french * more french * finish french * fix some missing translations * update readme * fix test --- CONTRIBUTING.md | 6 + README.md | 4 +- bin/build-template-html.js | 10 +- package.json | 12 +- src/build/template.html | 6 +- src/intl/en-US.js | 628 ++++++++++++++++++ src/intl/fr.js | 628 ++++++++++++++++++ .../_a11y/getAccessibleLabelForStatus.js | 15 +- src/routes/_actions/block.js | 10 +- src/routes/_actions/bookmark.js | 11 +- src/routes/_actions/compose.js | 5 +- src/routes/_actions/copyText.js | 2 +- src/routes/_actions/delete.js | 5 +- src/routes/_actions/favorite.js | 8 +- src/routes/_actions/follow.js | 12 +- src/routes/_actions/instances.js | 6 +- src/routes/_actions/media.js | 2 +- src/routes/_actions/mute.js | 12 +- src/routes/_actions/muteConversation.js | 12 +- src/routes/_actions/pin.js | 12 +- src/routes/_actions/polls.js | 5 +- src/routes/_actions/reblog.js | 8 +- src/routes/_actions/reportStatuses.js | 5 +- src/routes/_actions/requests.js | 12 +- src/routes/_actions/search.js | 3 +- src/routes/_actions/setDomainBlocked.js | 12 +- src/routes/_actions/setShowReblogs.js | 12 +- src/routes/_actions/share.js | 3 +- src/routes/_actions/timeline.js | 2 +- src/routes/_components/AccountsListPage.html | 3 +- src/routes/_components/DynamicPageBanner.html | 6 +- .../_components/InformationalFooter.html | 14 +- src/routes/_components/LengthIndicator.html | 5 +- src/routes/_components/LoadingSpinner.html | 2 +- src/routes/_components/NavItem.html | 20 +- src/routes/_components/NotLoggedInHome.html | 30 +- .../_components/NotificationFilters.html | 6 +- src/routes/_components/ShortcutHelpInfo.html | 77 +-- src/routes/_components/SvgIconLegacy.html | 36 - src/routes/_components/TabSet.html | 15 +- src/routes/_components/Title.html | 22 +- .../_components/community/PageListItem.html | 16 +- .../_components/compose/ComposeBox.html | 2 +- .../_components/compose/ComposeButton.html | 4 +- .../compose/ComposeContentWarning.html | 4 +- .../_components/compose/ComposeFileDrop.html | 4 +- .../_components/compose/ComposeInput.html | 6 +- .../_components/compose/ComposeMedia.html | 2 +- .../_components/compose/ComposeMediaItem.html | 12 +- .../compose/ComposeMediaSensitive.html | 2 +- .../_components/compose/ComposePoll.html | 19 +- .../_components/compose/ComposeToolbar.html | 20 +- .../AccountProfileOptionsDialog.html | 29 +- .../dialog/components/ConfirmationDialog.html | 4 +- .../dialog/components/CopyDialog.html | 2 +- .../dialog/components/MediaAltEditor.html | 13 +- .../dialog/components/MediaDialog.html | 28 +- .../dialog/components/MediaEditDialog.html | 2 +- .../components/MediaFocalPointEditor.html | 2 +- .../dialog/components/MuteDialog.html | 12 +- .../dialog/components/PinchZoomable.html | 4 +- .../dialog/components/ReportDialog.html | 37 +- .../dialog/components/ShortcutHelpDialog.html | 2 +- .../components/StatusOptionsDialog.html | 33 +- .../_components/profile/AccountProfile.html | 8 +- .../profile/AccountProfileDetails.html | 22 +- .../profile/AccountProfileFilters.html | 2 +- .../profile/AccountProfileFollow.html | 18 +- .../profile/AccountProfileHeader.html | 20 +- .../profile/AccountProfileMeta.html | 2 +- .../profile/AccountProfileMovedBanner.html | 8 +- .../profile/AccountProfileNote.html | 2 +- .../profile/AccountProfilePage.html | 10 +- src/routes/_components/search/Search.html | 6 +- .../_components/settings/SettingsNav.html | 10 +- .../_components/settings/SettingsNavItem.html | 10 +- .../instance/HomeTimelineFilterSettings.html | 6 +- .../settings/instance/InstanceActions.html | 9 +- .../instance/NotificationFilterSettings.html | 12 +- .../instance/PushNotificationSettings.html | 25 +- .../settings/instance/ThemeSettings.html | 15 +- src/routes/_components/snackbar/Snackbar.html | 4 +- src/routes/_components/status/Media.html | 12 +- .../_components/status/Notification.html | 6 +- .../_components/status/StatusDetails.html | 27 +- .../_components/status/StatusHeader.html | 14 +- .../status/StatusMediaAttachments.html | 6 +- src/routes/_components/status/StatusPoll.html | 21 +- .../status/StatusRelativeDate.html | 7 +- .../_components/status/StatusSpoiler.html | 2 +- .../_components/status/StatusToolbar.html | 16 +- .../_components/timeline/LoadingFooter.html | 4 +- .../_components/timeline/MoreHeader.html | 11 +- .../_components/timeline/PinnedStatuses.html | 4 +- .../_components/virtualList/VirtualList.html | 2 +- src/routes/_intl/formatTimeagoDate.js | 8 +- .../accounts/[accountId]/followers.html | 2 +- .../_pages/accounts/[accountId]/follows.html | 2 +- src/routes/_pages/blocked.html | 4 +- src/routes/_pages/bookmarks.html | 6 +- src/routes/_pages/community/index.html | 46 +- src/routes/_pages/direct.html | 6 +- src/routes/_pages/favorites.html | 6 +- src/routes/_pages/federated.html | 6 +- src/routes/_pages/lists/[listId].html | 6 +- src/routes/_pages/local.html | 6 +- src/routes/_pages/muted.html | 4 +- src/routes/_pages/notifications/index.html | 6 +- src/routes/_pages/notifications/mentions.html | 4 +- src/routes/_pages/pinned.html | 5 +- src/routes/_pages/requests.html | 6 +- src/routes/_pages/search.html | 16 +- src/routes/_pages/settings/about.html | 44 +- src/routes/_pages/settings/general.html | 57 +- src/routes/_pages/settings/hotkeys.html | 14 +- src/routes/_pages/settings/index.html | 16 +- .../settings/instances/[instanceName].html | 10 +- src/routes/_pages/settings/instances/add.html | 26 +- .../_pages/settings/instances/index.html | 24 +- src/routes/_pages/settings/wellness.html | 33 +- .../_pages/statuses/[statusId]/favorites.html | 4 +- .../_pages/statuses/[statusId]/index.html | 8 +- .../_pages/statuses/[statusId]/reblogs.html | 4 +- src/routes/_pages/tags/[tagName].html | 4 +- src/routes/_static/intl.js | 2 + src/routes/_static/polls.js | 17 +- src/routes/_static/statuses.js | 10 +- src/routes/_static/themes.js | 34 +- src/routes/_static/timelines.js | 6 +- .../_store/computations/navComputations.js | 20 +- .../_store/observers/onlineObservers.js | 2 +- src/routes/_thirdparty/timeago/timeago.js | 83 +-- src/routes/_utils/formatIntl.js | 27 + src/routes/_utils/formatters.js | 6 +- src/routes/_utils/serviceWorkerClient.js | 2 +- .../accounts/[accountId]/followers.html | 2 +- src/routes/accounts/[accountId]/follows.html | 2 +- src/routes/accounts/[accountId]/index.html | 2 +- src/routes/accounts/[accountId]/media.html | 2 +- .../accounts/[accountId]/with_replies.html | 2 +- src/routes/blocked.html | 2 +- src/routes/bookmarks.html | 2 +- src/routes/community/index.html | 2 +- src/routes/direct.html | 2 +- src/routes/favorites.html | 2 +- src/routes/federated.html | 2 +- src/routes/index.html | 2 +- src/routes/lists/[listId].html | 2 +- src/routes/local.html | 2 +- src/routes/muted.html | 2 +- src/routes/notifications/index.html | 2 +- src/routes/notifications/mentions.html | 2 +- src/routes/pinned.html | 2 +- src/routes/requests.html | 2 +- src/routes/search.html | 2 +- src/routes/settings/about.html | 2 +- src/routes/settings/general.html | 2 +- src/routes/settings/hotkeys.html | 2 +- src/routes/settings/index.html | 2 +- src/routes/settings/instances/add.html | 7 +- src/routes/settings/instances/index.html | 2 +- src/routes/settings/wellness.html | 2 +- src/routes/statuses/[statusId]/favorites.html | 2 +- src/routes/statuses/[statusId]/index.html | 2 +- src/routes/statuses/[statusId]/reblogs.html | 2 +- src/routes/tags/[tagName].html | 2 +- src/service-worker.js | 4 +- tests/spec/014-compose-post-privacy.js | 2 +- tests/spec/022-status-aria-label.js | 6 +- tests/spec/037-pin-timelines.js | 4 +- tests/spec/102-notifications.js | 2 +- tests/spec/108-compose-dialog.js | 4 +- tests/unit/test-intl.js | 31 + tests/utils.js | 4 +- webpack/client.config.js | 34 +- webpack/server.config.js | 16 +- webpack/service-worker.config.js | 17 +- webpack/shared.config.js | 7 +- webpack/svelte-intl-loader.js | 48 ++ yarn.lock | 44 +- 180 files changed, 2320 insertions(+), 846 deletions(-) create mode 100644 src/intl/en-US.js create mode 100644 src/intl/fr.js delete mode 100644 src/routes/_components/SvgIconLegacy.html create mode 100644 src/routes/_static/intl.js create mode 100644 src/routes/_utils/formatIntl.js create mode 100644 tests/unit/test-intl.js create mode 100644 webpack/svelte-intl-loader.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86f1a980..6f32534d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,11 @@ # Contributing to Pinafore +## Internationalization + +To contribute or change translations for Pinafore, look in the [src/intl](https://github.com/nolanlawson/pinafore/tree/master/src/intl) directory. Create a new file or edit an existing file based on its [two-letter language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and optionally, a region. For instance, `en-US.js` is American English, and `fr.js` is French. + +The default is `en-US.js`, and any strings not defined in a language file will fall back to the strings from that file. + ## Installing To install with dev dependencies, run: diff --git a/README.md b/README.md index 7c641982..1d82d983 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine. - Progressive Web App features - Multi-instance support - Support latest versions of Chrome, Edge, Firefox, and Safari +- Support non-Mastodon instances (e.g. Pleroma) as well as possible +- Internationalization ### Secondary / possible future goals -- Support for Pleroma or other non-Mastodon backends - Serve as an alternative frontend tied to a particular instance -- Support for non-English languages (i18n) - Offline search ### Non-goals diff --git a/bin/build-template-html.js b/bin/build-template-html.js index 22752a28..f10b16d7 100644 --- a/bin/build-template-html.js +++ b/bin/build-template-html.js @@ -7,9 +7,12 @@ import { buildInlineScript } from './build-inline-script' import { buildSvg } from './build-svg' import now from 'performance-now' import debounce from 'lodash-es/debounce' +import applyIntl from '../webpack/svelte-intl-loader' +import { LOCALE } from '../src/routes/_static/intl' +import { getLangDir } from 'rtl-detect' const writeFile = promisify(fs.writeFile) - +const LOCALE_DIRECTION = getLangDir(LOCALE) const DEBOUNCE = 500 const builders = [ @@ -78,7 +81,7 @@ function doWatch () { async function buildAll () { const start = now() - const html = (await Promise.all(partials.map(async partial => { + let html = (await Promise.all(partials.map(async partial => { if (typeof partial === 'string') { return partial } @@ -88,6 +91,9 @@ async function buildAll () { return partial.result }))).join('') + html = applyIntl(html) + .replace('{process.env.LOCALE}', LOCALE) + .replace('{process.env.LOCALE_DIRECTION}', LOCALE_DIRECTION) await writeFile(path.resolve(__dirname, '../src/template.html'), html, 'utf8') const end = now() console.log(`Built template.html in ${(end - start).toFixed(2)}ms`) diff --git a/package.json b/package.json index ed2c692f..0b65ff9b 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,11 @@ "lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'", "dev": "run-s build-template-html build-assets serve-dev", "serve-dev": "run-p --race build-template-html-watch sapper-dev", - "sapper-dev": "cross-env NODE_ENV=development PORT=4002 sapper dev", + "sapper-dev": "cross-env NODE_ENV=development PORT=4002 node -r esm ./node_modules/.bin/sapper dev", "before-build": "run-s build-template-html build-assets", "build": "cross-env NODE_ENV=production run-s build-steps", "build-steps": "run-s before-build sapper-export build-vercel-json", - "sapper-build": "sapper build", + "sapper-build": "node -r esm ./node_modules/.bin/sapper build", "start": "node server.js", "build-and-start": "run-s build start", "build-template-html": "node -r esm ./bin/build-template-html.js", @@ -25,13 +25,13 @@ "testcafe": "run-s testcafe-suite0 testcafe-suite1", "testcafe-suite0": "cross-env-shell testcafe -c 4 $BROWSER tests/spec/0*", "testcafe-suite1": "cross-env-shell testcafe $BROWSER tests/spec/1*", - "test-unit": "mocha -r esm -r bin/browser-shim.js tests/unit/", + "test-unit": "NODE_ENV=test mocha -r esm -r bin/browser-shim.js tests/unit/", "wait-for-mastodon-to-start": "node -r esm bin/wait-for-mastodon-to-start.js", "wait-for-mastodon-data": "node -r esm bin/wait-for-mastodon-data.js", "deploy-prod": "DEPLOY_TYPE=prod ./bin/deploy.sh", "deploy-dev": "DEPLOY_TYPE=dev ./bin/deploy.sh", "backup-mastodon-data": "./bin/backup-mastodon-data.sh", - "sapper-export": "cross-env PORT=22939 sapper export", + "sapper-export": "cross-env PORT=22939 node -r esm ./node_modules/.bin/sapper export", "print-export-info": "node ./bin/print-export-info.js", "export-steps": "run-s before-build sapper-export print-export-info", "export": "cross-env NODE_ENV=production run-s export-steps", @@ -62,6 +62,7 @@ "file-loader": "^6.1.0", "focus-visible": "^5.1.0", "form-data": "^3.0.0", + "format-message-interpret": "^6.2.3", "glob": "^7.1.6", "li": "^1.3.0", "localstorage-memory": "^1.0.3", @@ -80,6 +81,7 @@ "rollup": "^2.26.10", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-terser": "^7.0.2", + "rtl-detect": "^1.0.2", "sapper": "nolanlawson/sapper#for-pinafore-21", "sass": "^1.26.10", "stringz": "^2.1.0", @@ -101,6 +103,8 @@ "assert": "^2.0.0", "eslint-plugin-html": "^6.1.0", "fake-indexeddb": "^3.1.2", + "format-message-parse": "^6.2.3", + "globby": "^11.0.1", "husky": "^5.0.4", "lint-staged": "^10.3.0", "mocha": "^8.1.3", diff --git a/src/build/template.html b/src/build/template.html index b390f763..382a215a 100644 --- a/src/build/template.html +++ b/src/build/template.html @@ -1,10 +1,10 @@ - + - + %sapper.base% @@ -15,7 +15,7 @@ https://developers.google.com/web/fundamentals/native-hardware/fullscreen/ --> - + diff --git a/src/intl/en-US.js b/src/intl/en-US.js new file mode 100644 index 00000000..8489ea0a --- /dev/null +++ b/src/intl/en-US.js @@ -0,0 +1,628 @@ +export default { + // Home page, basic and <description> + appName: 'Pinafore', + appDescription: 'An alternative web client for Mastodon, focused on speed and simplicity.', + homeDescription: ` + <p> + Pinafore is a web client for + <a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>, + designed for speed and simplicity. + </p> + <p> + Read the + <a rel="noopener" target="_blank" + href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">introductory blog post</a>, + or get started by logging in to an instance: + </p>`, + logIn: 'Log in', + footer: ` + <p> + Pinafore is + <a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">open-source software</a> + created by + <a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a> + and distributed under the + <a rel="noopener" target="_blank" + href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">AGPL License</a>. + Here is the <a href="/settings/about#privacy-policy" rel="prefetch">privacy policy</a>. + </p> + `, + // Generic UI + loading: 'Loading', + okay: 'OK', + cancel: 'Cancel', + alert: 'Alert', + close: 'Close', + error: 'Error: {error}', + errorShort: 'Error:', + // Relative timestamps + justNow: 'just now', + // Navigation, page titles + navItemLabel: ` + {label} {selected, select, + true {(current page)} + other {} + } {name, select, + notifications {{count, plural, + =0 {} + one {(1 notification)} + other {({count} notifications)} + }} + community {{count, plural, + =0 {} + one {(1 follow request)} + other {({count} follow requests)} + }} + other {} + } + `, + blockedUsers: 'Blocked users', + bookmarks: 'Bookmarks', + directMessages: 'Direct messages', + favorites: 'Favorites', + federated: 'Federated', + home: 'Home', + local: 'Local', + notifications: 'Notifications', + mutedUsers: 'Muted users', + pinnedStatuses: 'Pinned toots', + followRequests: 'Follow requests', + followRequestsLabel: `Follow requests {hasFollowRequests, select, + true {({count})} + other {} + }`, + list: 'List', + search: 'Search', + pageHeader: 'Page header', + goBack: 'Go back', + back: 'Back', + profile: 'Profile', + federatedTimeline: 'Federated timeline', + localTimeline: 'Local timeline', + // community page + community: 'Community', + pinnableTimelines: 'Pinnable timelines', + timelines: 'Timelines', + lists: 'Lists', + instanceSettings: 'Instance settings', + notificationMentions: 'Notification mentions', + profileWithMedia: 'Profile with media', + profileWithReplies: 'Profile with replies', + hashtag: 'Hashtag', + // not logged in + profileNotLoggedIn: 'A user timeline will appear here when logged in.', + bookmarksNotLoggedIn: 'Your bookmarks will appear here when logged in.', + directMessagesNotLoggedIn: 'Your direct messages will appear here when logged in.', + favoritesNotLoggedIn: 'Your favorites will appear here when logged in.', + federatedTimelineNotLoggedIn: 'Your federated timeline will appear here when logged in.', + localTimelineNotLoggedIn: 'Your local timeline will appear here when logged in.', + searchNotLoggedIn: 'You can search once logged in to an instance.', + communityNotLoggedIn: 'Community options appear here when logged in.', + listNotLoggedIn: 'A list will appear here when logged in.', + notificationsNotLoggedIn: 'Your notifications will appear here when logged in.', + notificationMentionsNotLoggedIn: 'Your notification mentions will appear here when logged in.', + statusNotLoggedIn: 'A toot thread will appear here when logged in.', + tagNotLoggedIn: 'A hashtag timeline will appear here when logged in.', + // Notification subpages + filters: 'Filters', + all: 'All', + mentions: 'Mentions', + // Follow requests + approve: 'Approve', + reject: 'Reject', + // Hotkeys + hotkeys: 'Hotkeys', + global: 'Global', + timeline: 'Timeline', + media: 'Media', + globalHotkeys: ` + {leftRightChangesFocus, select, + true { + <li><kbd>→</kbd> to go to the next focusable element</li> + <li><kbd>←</kbd> to go to the previous focusable element</li> + } + other {} + } + <li> + <kbd>1</kbd> - <kbd>6</kbd> + {leftRightChangesFocus, select, + true {} + other {or <kbd>←</kbd>/<kbd>→</kbd>} + } + to switch columns + </li> + <li><kbd>7</kbd> or <kbd>c</kbd> to compose a new toot</li> + <li><kbd>s</kbd> or <kbd>/</kbd> to search</li> + <li><kbd>g</kbd> + <kbd>h</kbd> to go home</li> + <li><kbd>g</kbd> + <kbd>n</kbd> to go to notifications</li> + <li><kbd>g</kbd> + <kbd>l</kbd> to go to the local timeline</li> + <li><kbd>g</kbd> + <kbd>t</kbd> to go to the federated timeline</li> + <li><kbd>g</kbd> + <kbd>c</kbd> to go to the community page</li> + <li><kbd>g</kbd> + <kbd>d</kbd> to go to the direct messages page</li> + <li><kbd>h</kbd> or <kbd>?</kbd> to toggle the help dialog</li> + <li><kbd>Backspace</kbd> to go back, close dialogs</li> + `, + timelineHotkeys: ` + <li><kbd>j</kbd> or <kbd>↓</kbd> to activate the next toot</li> + <li><kbd>k</kbd> or <kbd>↑</kbd> to activate the previous toot</li> + <li><kbd>.</kbd> to show more and scroll to top</li> + <li><kbd>o</kbd> to open</li> + <li><kbd>f</kbd> to favorite</li> + <li><kbd>b</kbd> to boost</li> + <li><kbd>r</kbd> to reply</li> + <li><kbd>i</kbd> to open images, video, or audio</li> + <li><kbd>y</kbd> to show or hide sensitive media</li> + <li><kbd>m</kbd> to mention the author</li> + <li><kbd>p</kbd> to open the author's profile</li> + <li><kbd>l</kbd> to open the card's link in a new tab</li> + <li><kbd>x</kbd> to show or hide text behind content warning</li> + `, + mediaHotkeys: ` + <li><kbd>←</kbd> / <kbd>→</kbd> to go to next or previous</li> + `, + // Community page, tabs + tabLabel: `{label} {current, select, + true {(Current)} + other {} + }`, + pageTitle: ` + {hasNotifications, select, + true {({count})} + other {} + } + {showInstanceName, select, + true {{instanceName}} + other {Pinafore} + } + · + {name} + `, + pinLabel: `{label} {pinnable, select, + true { + {pinned, select, + true {(Pinned page)} + other {(Unpinned page)} + } + } + other {} + }`, + pinPage: 'Pin {label}', + // Status composition + overLimit: '{count} {count, plural, =1 {character} other {characters}} over limit', + underLimit: '{count} {count, plural, =1 {character} other {characters}} remaining', + composeStatus: 'Compose toot', + postStatus: 'Toot!', + contentWarning: 'Content warning', + dropToUpload: 'Drop to upload', + invalidFileType: 'Invalid file type', + composeLabel: "What's on your mind?", + autocompleteDescription: 'When autocomplete results are available, press up or down arrows and enter to select.', + mediaUploads: 'Media uploads', + edit: 'Edit', + delete: 'Delete', + description: 'Description', + descriptionLabel: 'Describe for the visually impaired (image, video) or auditorily impaired (audio, video)', + markAsSensitive: 'Mark media as sensitive', + // Polls + createPoll: 'Create poll', + removePollChoice: 'Remove choice {index}', + pollChoiceLabel: 'Choice {index}', + multipleChoice: 'Multiple choice', + pollDuration: 'Poll duration', + fiveMinutes: '5 minutes', + thirtyMinutes: '30 minutes', + oneHour: '1 hour', + sixHours: '6 hours', + oneDay: '1 day', + threeDays: '3 days', + sevenDays: '7 days', + addEmoji: 'Insert emoji', + addMedia: 'Add media (images, video, audio)', + addPoll: 'Add poll', + removePoll: 'Remove poll', + postPrivacyLabel: 'Adjust privacy (currently {label})', + addContentWarning: 'Add content warning', + removeContentWarning: 'Remove content warning', + altLabel: 'Describe for the visually impaired', + extractText: 'Extract text from image', + extractingText: 'Extracting text…', + extractingTextCompletion: 'Extracting text ({percent}% complete)…', + unableToExtractText: 'Unable to extract text.', + // Account options + followAccount: 'Follow {account}', + unfollowAccount: 'Unfollow {account}', + blockAccount: 'Block {account}', + unblockAccount: 'Unblock {account}', + muteAccount: 'Mute {account}', + unmuteAccount: 'Unmute {account}', + showReblogsFromAccount: 'Show boosts from {account}', + hideReblogsFromAccount: 'Hide boosts from {account}', + showDomain: 'Unhide {domain}', + hideDomain: 'Hide {domain}', + reportAccount: 'Report {account}', + mentionAccount: 'Mention {account}', + copyLinkToAccount: 'Copy link to account', + copiedToClipboard: 'Copied to clipboard', + // Media dialog + navigateMedia: 'Navigate media items', + showPreviousMedia: 'Show previous media', + showNextMedia: 'Show next media', + enterPinchZoom: 'Pinch-zoom mode', + exitPinchZoom: 'Exit pinch-zoom mode', + showMedia: `Show {index, select, + 1 {first} + 2 {second} + 3 {third} + other {fourth} + } media {current, select, + true {(current)} + other {} + }`, + previewFocalPoint: 'Preview (focal point)', + enterFocalPoint: 'Enter the focal point (X, Y) for this media', + muteNotifications: 'Mute notifications as well', + muteAccountConfirm: 'Mute {account}?', + mute: 'Mute', + unmute: 'Unmute', + zoomOut: 'Zoom out', + zoomIn: 'Zoom in', + // Reporting + reportingLabel: 'You are reporting {account} to the moderators of {instance}.', + additionalComments: 'Additional comments', + forwardDescription: 'Forward to the moderators of {instance} as well?', + forwardLabel: 'Forward to {instance}', + unableToLoadStatuses: 'Unable to load recent toots: {error}', + report: 'Report', + noContent: '(No content)', + noStatuses: 'No toots to report', + // Status options + unpinFromProfile: 'Unpin from profile', + pinToProfile: 'Pin to profile', + muteConversation: 'Mute conversation', + unmuteConversation: 'Unmute conversation', + bookmarkStatus: 'Bookmark toot', + unbookmarkStatus: 'Unbookmark toot', + deleteAndRedraft: 'Delete and redraft', + reportStatus: 'Report toot', + shareStatus: 'Share toot', + copyLinkToStatus: 'Copy link to toot', + // Account profile + profileForAccount: 'Profile for {account}', + statisticsAndMoreOptions: 'Stats and more options', + statuses: 'Toots', + follows: 'Follows', + followers: 'Followers', + moreOptions: 'More options', + followersLabel: 'Followed by {count}', + followingLabel: 'Follows {count}', + followLabel: `Follow {requested, select, + true {(follow requested)} + other {} + }`, + unfollowLabel: `Unfollow {requested, select, + true {(follow requested)} + other {} + }`, + unblock: 'Unblock', + nameAndFollowing: 'Name and following', + clickToSeeAvatar: 'Click to see avatar', + opensInNewWindow: '{label} (opens in new window)', + blocked: 'Blocked', + domainHidden: 'Domain hidden', + muted: 'Muted', + followsYou: 'Follows you', + avatarForAccount: 'Avatar for {account}', + fields: 'Fields', + accountHasMoved: '{account} has moved:', + profilePageForAccount: 'Profile page for {account}', + // About page + about: 'About', + aboutApp: 'About Pinafore', + aboutAppDescription: ` + <p> + Pinafore is + <a rel="noopener" target="_blank" + href="https://github.com/nolanlawson/pinafore">free and open-source software</a> + created by + <a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a> + and distributed under the + <a rel="noopener" target="_blank" + href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>. + </p> + + <h2 id="privacy-policy">Privacy Policy</h2> + + <p> + Pinafore does not store any personal information on its servers, + including but not limited to names, email addresses, + IP addresses, posts, and photos. + </p> + + <p> + Pinafore is a static site. All data is stored locally in your browser and shared with the fediverse + instance(s) you connect to. + </p> + + <h2>Credits</h2> + + <p> + Icons provided by <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>. + </p> + + <p> + Logo thanks to "sailboat" by Gregor Cresnar from + <a rel="noopener" target="_blank" href="https://thenounproject.com/">the Noun Project</a>. + </p>`, + // Settings + settings: 'Settings', + general: 'General', + generalSettings: 'General settings', + showSensitive: 'Show sensitive media by default', + showPlain: 'Show a plain gray color for sensitive media', + allSensitive: 'Treat all media as sensitive', + largeMedia: 'Show large inline images and videos', + autoplayGifs: 'Autoplay animated GIFs', + hideCards: 'Hide link preview cards', + underlineLinks: 'Underline links in toots and profiles', + accessibility: 'Accessibility', + reduceMotion: 'Reduce motion in UI animations', + disableTappable: 'Disable tappable area on entire toot', + removeEmoji: 'Remove emoji from user display names', + shortAria: 'Use short article ARIA labels', + theme: 'Theme', + themeForInstance: 'Theme for {instance}', + disableCustomScrollbars: 'Disable custom scrollbars', + preferences: 'Preferences', + hotkeySettings: 'Hotkey settings', + disableHotkeys: 'Disable all hotkeys', + leftRightArrows: 'Left/right arrow keys change focus rather than columns/media', + guide: 'Guide', + reload: 'Reload', + // Wellness settings + wellness: 'Wellness', + wellnessSettings: 'Wellness settings', + wellnessDescription: `Wellness settings are designed to reduce the addictive or anxiety-inducing aspects of social media. + Choose any options that work well for you.`, + enableAll: 'Enable all', + metrics: 'Metrics', + hideFollowerCount: 'Hide follower counts (capped at 10)', + hideReblogCount: 'Hide boost counts', + hideFavoriteCount: 'Hide favorite counts', + hideUnread: 'Hide unread notifications count (i.e. the red dot)', + ui: 'UI', + grayscaleMode: 'Grayscale mode', + wellnessFooter: `These settings are partly based on guidelines from the + <a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`, + // This is a link: "You can filter or disable notifications in the _instance settings_" + filterNotificationsPre: 'You can filter or disable notifications in the', + filterNotificationsText: 'instance settings', + filterNotificationsPost: '', + // Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_ + // to see a description. It's hard to properly internationalize, so we just break up the strings. + disableInfiniteScrollPre: 'Disable', + disableInfiniteScrollText: 'infinite scroll', + disableInfiniteScrollDescription: `When infinite scroll is disabled, new toots will not automatically appear at + the bottom or top of the timeline. Instead, buttons will allow you to + load more content on demand.`, + disableInfiniteScrollPost: '', + // Instance settings + loggedInAs: 'Logged in as', + homeTimelineFilters: 'Home timeline filters', + notificationFilters: 'Notification filters', + pushNotifications: 'Push notifications', + // Add instance page + storageError: `It seems Pinafore cannot store data locally. Is your browser in private mode + or blocking cookies? Pinafore stores all data locally, and requires LocalStorage and + IndexedDB to work correctly.`, + javaScriptError: 'You must enable JavaScript to log in.', + enterInstanceName: 'Enter instance name', + instanceColon: 'Instance:', + // Custom tooltip, concatenated together + getAnInstancePre: "Don't have an", + getAnInstanceText: 'instance', + getAnInstanceDescription: 'An instance is your Mastodon home server, such as mastodon.social or cybre.space.', + getAnInstancePost: '?', + joinMastodon: 'Join Mastodon!', + instancesYouveLoggedInTo: "Instances you've logged in to:", + addAnotherInstance: 'Add another instance', + youreNotLoggedIn: "You're not logged in to any instances.", + currentInstanceLabel: `{instance} {current, select, + true {(current instance)} + other {} + }`, + // Link text + logInToAnInstancePre: '', + logInToAnInstanceText: 'Log in to an instance', + logInToAnInstancePost: 'to start using Pinafore.', + // Another custom tooltip + showRingPre: 'Always show', + showRingText: 'focus ring', + showRingDescription: `The focus ring is the outline showing the currently focused element. By default, it's only + shown when using the keyboard (not mouse or touch), but you may choose to always show it.`, + showRingPost: '', + instances: 'Instances', + addInstance: 'Add instance', + homeTimelineFilterSettings: 'Home timeline filter settings', + showReblogs: 'Show boosts', + showReplies: 'Show replies', + switchOrLogOut: 'Switch to or log out of this instance', + switchTo: 'Switch to this instance', + switchToInstance: 'Switch to instance', + switchToNameOfInstance: 'Switch to {instance}', + logOut: 'Log out', + logOutOfInstanceConfirm: 'Log out of {instance}?', + notificationFilterSettings: 'Notification filter settings', + // Push notifications + browserDoesNotSupportPush: "Your browser doesn't support push notifications.", + deniedPush: 'You have denied permission to show notifications.', + pushNotificationsNote: 'Note that you can only have push notifications for one instance at a time.', + pushSettings: 'Push notification settings', + newFollowers: 'New followers', + reblogs: 'Boosts', + pollResults: 'Poll results', + needToReauthenticate: 'You need to reauthenticate in order to enable push notification. Log out of {instance}?', + failedToUpdatePush: 'Failed to update push notification settings: {error}', + // Themes + chooseTheme: 'Choose a theme', + darkBackground: 'Dark background', + lightBackground: 'Light background', + themeLabel: `{label} {default, select, + true {(default)} + other {} + }`, + animatedImage: 'Animated image: {description}', + showImage: `Show {animated, select, + true {animated} + other {} + } image: {description}`, + playVideoOrAudio: `Play {audio, select, + true {audio} + other {video} + }: {description}`, + accountFollowedYou: '{name} followed you, {account}', + reblogCountsHidden: 'Boost counts hidden', + favoriteCountsHidden: 'Favorite counts hidden', + rebloggedTimes: `Boosted {count, plural, + one {1 time} + other {{count} times} + }`, + favoritedTimes: `Favorited {count, plural, + one {1 time} + other {{count} times} + }`, + pinnedStatus: 'Pinned toot', + rebloggedYou: 'boosted your toot', + favoritedYou: 'favorited your toot', + followedYou: 'followed you', + pollYouCreatedEnded: 'A poll you created has ended', + pollYouVotedEnded: 'A poll you voted on has ended', + reblogged: 'boosted', + showSensitiveMedia: 'Show sensitive media', + hideSensitiveMedia: 'Hide sensitive media', + clickToShowSensitive: 'Sensitive content. Click to show.', + longPost: 'Long post', + // Accessible status labels + accountRebloggedYou: '{account} boosted your toot', + accountFavoritedYou: '{account} favorited your toot', + rebloggedByAccount: 'Boosted by {account}', + contentWarningContent: 'Content warning: {spoiler}', + hasMedia: 'has media', + hasPoll: 'has poll', + shortStatusLabel: '{privacy} toot by {account}', + // Privacy types + public: 'Public', + unlisted: 'Unlisted', + followersOnly: 'Followers-only', + direct: 'Direct', + // Themes + themeRoyal: 'Royal', + themeScarlet: 'Scarlet', + themeSeafoam: 'Seafoam', + themeHotpants: 'Hotpants', + themeOaken: 'Oaken', + themeMajesty: 'Majesty', + themeGecko: 'Gecko', + themeGrayscale: 'Grayscale', + themeOzark: 'Ozark', + themeCobalt: 'Cobalt', + themeSorcery: 'Sorcery', + themePunk: 'Punk', + themeRiot: 'Riot', + themeHacker: 'Hacker', + themeMastodon: 'Mastodon', + themePitchBlack: 'Pitch Black', + themeDarkGrayscale: 'Dark Grayscale', + // Polls + voteOnPoll: 'Vote on poll', + pollChoices: 'Poll choices', + vote: 'Vote', + pollDetails: 'Poll details', + refresh: 'Refresh', + expires: 'Ends', + expired: 'Ended', + voteCount: `{count, plural, + one {1 vote} + other {{count} votes} + }`, + // Status interactions + clickToShowThread: '{time} - click to show thread', + showMore: 'Show more', + showLess: 'Show less', + closeReply: 'Close reply', + cannotReblogFollowersOnly: 'Cannot be boosted because this is followers-only', + cannotReblogDirectMessage: 'Cannot be boosted because this is a direct message', + reblog: 'Boost', + reply: 'Reply', + replyToThread: 'Reply to thread', + favorite: 'Favorite', + unfavorite: 'Unfavorite', + // timeline + loadingMore: 'Loading more…', + loadMore: 'Load more', + showCountMore: 'Show {count} more', + nothingToShow: 'Nothing to show.', + // status thread page + statusThreadPage: 'Toot thread page', + status: 'Toot', + // toast messages + blockedAccount: 'Blocked account', + unblockedAccount: 'Unblocked account', + unableToBlock: 'Unable to block account: {error}', + unableToUnblock: 'Unable to unblock account: {error}', + bookmarkedStatus: 'Bookmarked toot', + unbookmarkedStatus: 'Unbookmarked toot', + unableToBookmark: 'Unable to bookmark: {error}', + unableToUnbookmark: 'Unable to unbookmark: {error}', + cannotPostOffline: 'You cannot post while offline', + unableToPost: 'Unable to post toot: {error}', + statusDeleted: 'Toot deleted', + unableToDelete: 'Unable to delete toot: {error}', + cannotFavoriteOffline: 'You cannot favorite while offline', + cannotUnfavoriteOffline: 'You cannot unfavorite while offline', + unableToFavorite: 'Unable to favorite: {error}', + unableToUnfavorite: 'Unable to unfavorite: {error}', + followedAccount: 'Followed account', + unfollowedAccount: 'Unfollowed account', + unableToFollow: 'Unable to follow account: {error}', + unableToUnfollow: 'Unable to unfollow account: {error}', + accessTokenRevoked: 'The access token was revoked, logged out of {instance}', + loggedOutOfInstance: 'Logged out of {instance}', + failedToUploadMedia: 'Failed to upload media: {error}', + mutedAccount: 'Muted account', + unmutedAccount: 'Unmuted account', + unableToMute: 'Unable to mute account: {error}', + unableToUnmute: 'Unable to unmute account: {error}', + mutedConversation: 'Muted conversation', + unmutedConversation: 'Unmuted conversation', + unableToMuteConversation: 'Unable to mute conversation: {error}', + unableToUnmuteConversation: 'Unable to unmute conversation: {error}', + unpinnedStatus: 'Unpinned toot', + unableToPinStatus: 'Unable to pin toot: {error}', + unableToUnpinStatus: 'Unable to unpin toot: {error}', + unableToRefreshPoll: 'Unable to refresh poll: {error}', + unableToVoteInPoll: 'Unable to vote in poll: {error}', + cannotReblogOffline: 'You cannot boost while offline.', + cannotUnreblogOffline: 'You cannot unboost while offline.', + failedToReblog: 'Failed to boost: {error}', + failedToUnreblog: 'Failed to unboost: {error}', + submittedReport: 'Submitted report', + failedToReport: 'Failed to report: {error}', + approvedFollowRequest: 'Approved follow request', + rejectedFollowRequest: 'Rejected follow request', + unableToApproveFollowRequest: 'Unable to approve follow request: {error}', + unableToRejectFollowRequest: 'Unable to reject follow request: {error}', + searchError: 'Error during search: {error}', + hidDomain: 'Hid domain', + unhidDomain: 'Unhid domain', + unableToHideDomain: 'Unable to hide domain: {error}', + unableToUnhideDomain: 'Unable to unhide domain: {error}', + showingReblogs: 'Showing boosts', + hidingReblogs: 'Hiding boosts', + unableToShowReblogs: 'Unable to show boosts: {error}', + unableToHideReblogs: 'Unable to hide boosts: {error}', + unableToShare: 'Unable to share: {error}', + showingOfflineContent: 'Internet request failed. Showing offline content.', + youAreOffline: 'You seem to be offline. You can still read toots while offline.', + // Snackbar UI + updateAvailable: 'App update available.' +} diff --git a/src/intl/fr.js b/src/intl/fr.js new file mode 100644 index 00000000..0acde0ac --- /dev/null +++ b/src/intl/fr.js @@ -0,0 +1,628 @@ +export default { + // Home page, basic <title> and <description> + appName: 'Pinafore', + appDescription: 'Un client alternatif pour Mastodon, concentré sur la vitesse et la simplicité', + homeDescription: ` + <p> + Pinafore est un client web pour + <a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>, + dessiné pour la vitesse et la simplicité. + </p> + <p> + Lire + <a rel="noopener" target="_blank" + href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">l'article introductoire (anglais)</a>, + ou se connecter à une instance: + </p>`, + logIn: 'Se connecter', + footer: ` + <p> + Pinafore est + <a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">logiciel open-source</a> + créé par + <a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a> + et distribué sous la + <a rel="noopener" target="_blank" + href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">License AGPL</a>. + Lire la <a href="/settings/about#privacy-policy" rel="prefetch">politique de confidentialité</a>. + </p> + `, + // Generic UI + loading: 'Chargement en cours', + okay: 'OK', + cancel: 'Annuler', + alert: 'Alerte', + close: 'Fermer', + error: 'Erreur: {error}', + errorShort: 'Erreur:', + // Relative timestamps + justNow: 'il y a un moment', + // Navigation, page titles + navItemLabel: ` + {label} {selected, select, + true {(page actuelle)} + other {} + } {name, select, + notifications {{count, plural, + =0 {} + one {(1 notification)} + other {({count} notifications)} + }} + community {{count, plural, + =0 {} + one {(1 demande de suivre)} + other {({count} demandes de suivre)} + }} + other {} + } + `, + blockedUsers: 'Utilisateurs bloqués', + bookmarks: 'Signets', + directMessages: 'Messages directs', + favorites: 'Favoris', + federated: 'Fédéré', + home: 'Accueil', + local: 'Local', + notifications: 'Notifications', + mutedUsers: 'Utilisateurs mis en sourdine', + pinnedStatuses: 'Pouets épinglés', + followRequests: 'Demandes de suivre', + followRequestsLabel: `Demandes de suivre {hasFollowRequests, select, + true {({count})} + other {} + }`, + list: 'Liste', + search: 'Recherche', + pageHeader: 'Titre de page', + goBack: 'Rentrer', + back: 'Rentrer', + profile: 'Profil', + federatedTimeline: 'Historique fédéré', + localTimeline: 'Historique local', + // community page + community: 'Communauté', + pinnableTimelines: 'Historiques épinglables', + timelines: 'Historiques', + lists: 'Listes', + instanceSettings: "Paramètres d'instance", + notificationMentions: 'Notifications de mention', + profileWithMedia: 'Profil avec medias', + profileWithReplies: 'Profil avec réponses', + hashtag: 'Mot-dièse', + // not logged in + profileNotLoggedIn: "Un historique d'utilisateur s'apparêtra ici quand on est conncté.", + bookmarksNotLoggedIn: "Vos signets s'apparêtront ici quand on est conncté.", + directMessagesNotLoggedIn: "Vos messages directes s'apparêtront ici quand on est conncté.", + favoritesNotLoggedIn: "Vos favoris s'apparêtront ici quand on est conncté.", + federatedTimelineNotLoggedIn: "L'historique fédéré s'apparêtra ici quand on est conncté.", + localTimelineNotLoggedIn: "L'historique local s'apparêtra ici quand on est conncté.", + searchNotLoggedIn: "On peut rechercher dès qu'on est conncté.", + communityNotLoggedIn: "Les paramètres de commnautés s'apparêtront ici quand on est conncté.", + listNotLoggedIn: "Une liste s'apparêtra ici dès qu'on est conncté.", + notificationsNotLoggedIn: "Vos notifications s'apparêtront ici quand on est conncté.", + notificationMentionsNotLoggedIn: "Vos notifications de mention s'apparêtront ici quand on est conncté.", + statusNotLoggedIn: "Un historique de pouet s'apparêtra ici quand on est conncté.", + tagNotLoggedIn: "Un historique de mot-dièse s'apparêtra ici quand on est conncté.", + // Notification subpages + filters: 'Filtres', + all: 'Tous', + mentions: 'Mentions', + // Follow requests + approve: 'Accepter', + reject: 'Rejeter', + // Hotkeys + hotkeys: 'Raccourcis clavier', + global: 'Global', + timeline: 'Historique', + media: 'Medias', + globalHotkeys: ` + {leftRightChangesFocus, select, + true { + <li><kbd>→</kbd> pour changer de focus à l'élément suivant</li> + <li><kbd>←</kbd> pour changer de focus à l'élément précédent</li> + } + other {} + } + <li> + <kbd>1</kbd> - <kbd>6</kbd> + {leftRightChangesFocus, select, + true {} + other {ou <kbd>←</kbd>/<kbd>→</kbd>} + } + pour changer de pages + </li> + <li><kbd>7</kbd> or <kbd>c</kbd> pour écrire un nouveau pouet</li> + <li><kbd>s</kbd> or <kbd>/</kbd> pour rechercher</li> + <li><kbd>g</kbd> + <kbd>h</kbd> pour renter à l'acceuil</li> + <li><kbd>g</kbd> + <kbd>n</kbd> pour voir les notifications</li> + <li><kbd>g</kbd> + <kbd>l</kbd> pour voir l'historique local</li> + <li><kbd>g</kbd> + <kbd>t</kbd> pour voir l'historique fédéré</li> + <li><kbd>g</kbd> + <kbd>c</kbd> pour voir les paramètres de communauté</li> + <li><kbd>g</kbd> + <kbd>d</kbd> pour voir les messages directs</li> + <li><kbd>h</kbd> ou <kbd>?</kbd> pour voir les raccourcis clavier</li> + <li><kbd>Retour arrière</kbd> pour rentrer à la page précédente, ou fermer une boite de dialogue</li> + `, + timelineHotkeys: ` + <li><kbd>j</kbd> ou <kbd>↓</kbd> pour activer le pouet suivant</li> + <li><kbd>k</kbd> ou <kbd>↑</kbd> pour activer le pouet précedent</li> + <li><kbd>.</kbd> pour afficher les nouveaus messages et renter en haut</li> + <li><kbd>o</kbd> pour ouvrir</li> + <li><kbd>f</kbd> pour ajouter aux favoris</li> + <li><kbd>b</kbd> pour partager</li> + <li><kbd>r</kbd> pour répondre</li> + <li><kbd>i</kbd> pour voir une image, vidéo, ou audio</li> + <li><kbd>y</kbd> pour afficher ou cacher une image sensible</li> + <li><kbd>m</kbd> pour mentionner l'auteur</li> + <li><kbd>p</kbd> pour voir le profile de l'auteur</li> + <li><kbd>l</kbd> pour ouvrir un lien de carte dans un nouvel onglet</li> + <li><kbd>x</kbd> pour afficher ou cacher le texte caché derrière une avertissement</li> + `, + mediaHotkeys: ` + <li><kbd>←</kbd> / <kbd>→</kbd> pour voir la prochaine ou dernière image</li> + `, + // Community page, tabs + tabLabel: `{label} {current, select, + true {(Actuel)} + other {} + }`, + pageTitle: ` + {hasNotifications, select, + true {({count})} + other {} + } + {showInstanceName, select, + true {{instanceName}} + other {Pinafore} + } + · + {name} + `, + pinLabel: `{label} {pinnable, select, + true { + {pinned, select, + true {(Page épinglée)} + other {(Page non-épinglée)} + } + } + other {} + }`, + pinPage: 'Epingler {label}', + // Status composition + overLimit: '{count} {count, plural, =1 {caractère} other {caractères}} en dessus de la limite', + underLimit: '{count} {count, plural, =1 {caractère} other {caractères}} qui reste', + composeStatus: 'Ecrire un pouet', + postStatus: 'Pouet!', + contentWarning: 'Avertissement', + dropToUpload: 'Déposer', + invalidFileType: "Impossible d'uploader ce type de fichier", + composeLabel: "Qu'avez vous en tête?", + autocompleteDescription: 'Quand les résultats sont dispibles, appuyez la fleche vers le haut ou vers le bas pour selectionner.', + mediaUploads: 'Medias uploadés', + edit: 'Rediger', + delete: 'Supprimer', + description: 'Déscription', + descriptionLabel: 'Décrire pour les aveugles (image, video) ou les sourds (audio, video)', + markAsSensitive: 'Désigner comme sensible', + // Polls + createPoll: 'Créer une enquête', + removePollChoice: 'Supprimer la choix {index}', + pollChoiceLabel: 'Choix {index}', + multipleChoice: 'Choix multiple', + pollDuration: "Duration de l'enquête", + fiveMinutes: '5 minutes', + thirtyMinutes: '30 minutes', + oneHour: '1 heure', + sixHours: '6 heures', + oneDay: '1 jour', + threeDays: '3 jours', + sevenDays: '7 jours', + addEmoji: 'Insérer un emoji', + addMedia: 'Ajouter un media (images, vidéos, audios)', + addPoll: 'Ajouter une enquête', + removePoll: "Enlever l'enquête", + postPrivacyLabel: 'Changer de confidentialité (actuellement {label})', + addContentWarning: 'Ajouter une avertissement', + removeContentWarning: "Enlever l'avertissement", + altLabel: 'Décrire pour les aveugles ou les sourds', + extractText: "Extraire le texte de l'image", + extractingText: 'Extraction de texte en cours…', + extractingTextCompletion: 'Extraction de texte en cours ({percent}% finit)…', + unableToExtractText: "Impossible d'extraire le texte.", + // Account options + followAccount: 'Suivre {account}', + unfollowAccount: 'Ne plus suivre {account}', + blockAccount: 'Bloquer {account}', + unblockAccount: 'Ne plus bloquer {account}', + muteAccount: 'Mettre {account} en sourdine', + unmuteAccount: 'Ne plus mettre {account} en sourdine', + showReblogsFromAccount: 'Afficher les partages de {account}', + hideReblogsFromAccount: 'Ne plus afficher les partages de {account}', + showDomain: 'Ne plus cacher {domain}', + hideDomain: 'Cacher {domain}', + reportAccount: 'Signaler {account}', + mentionAccount: 'Mentionner {account}', + copyLinkToAccount: 'Copier un lien vers ce compte', + copiedToClipboard: 'Copié vers le presse-papiers', + // Media dialog + navigateMedia: 'Changer de medias', + showPreviousMedia: 'Afficher le media précédent', + showNextMedia: 'Afficher le media suivant', + enterPinchZoom: 'Pincer pour zoomer', + exitPinchZoom: 'Ne plus pincer pour zoomer', + showMedia: `Afficher le {index, select, + 1 {premier} + 2 {deuxième} + 3 {troisième} + other {quatrième} + } média {current, select, + true {(actuel)} + other {} + }`, + previewFocalPoint: 'Aperçu (point de mire)', + enterFocalPoint: 'Saisir le point de mire (X, Y) pour ce média', + muteNotifications: 'Mettre aussi bien les notifications en sourdine', + muteAccountConfirm: 'Mettre {account} en sourdine?', + mute: 'Mettre en sourdine', + unmute: 'Ne plus mettre en sourdine', + zoomOut: 'Dé-zoomer', + zoomIn: 'Zoomer', + // Reporting + reportingLabel: 'Vous signalez {account} aux modérateurs/modératrices de {instance}.', + additionalComments: 'Commentaires additionels', + forwardDescription: 'Faire parvenir aux modérateurs/modératrices de {instance} aussi?', + forwardLabel: 'Fair pervenir à {instance}', + unableToLoadStatuses: 'Impossible de charger les pouets récents: {error}', + report: 'Signaler', + noContent: '(Pas de contenu)', + noStatuses: 'Aucun pouet à signaler', + // Status options + unpinFromProfile: 'Ne plus épingler sur son profil', + pinToProfile: 'Epingler sur son profil', + muteConversation: 'Mettre en sourdine la conversation', + unmuteConversation: 'Ne plus mettre en sourdine la conversation', + bookmarkStatus: 'Ajouter aux signets', + unbookmarkStatus: 'Enlever des signets', + deleteAndRedraft: 'Supprimer et rediger', + reportStatus: 'Signaler ce pouet', + shareStatus: 'Partager ce pouet externellement', + copyLinkToStatus: 'Copier un lien vers ce pouet', + // Account profile + profileForAccount: 'Profil pour {account}', + statisticsAndMoreOptions: "Statistiques et plus d'options", + statuses: 'Pouets', + follows: 'Suis', + followers: 'Suivants', + moreOptions: "Plus d'options", + followersLabel: 'Suivi(e) par {count}', + followingLabel: 'Suis {count}', + followLabel: `Suivre {requested, select, + true {(suivre demandé)} + other {} + }`, + unfollowLabel: `Ne plus suivre {requested, select, + true {(suivre demandé)} + other {} + }`, + unblock: 'Ne plus bloquer', + nameAndFollowing: 'Nom et suivants', + clickToSeeAvatar: "Cliquer pour voir l'image de profile", + opensInNewWindow: '{label} (ouvrir dans un nouvel onglet)', + blocked: 'Bloquer', + domainHidden: 'Domaine bloqué', + muted: 'Mis en sourdine', + followsYou: 'Suivant', + avatarForAccount: 'Image de profil pour {account}', + fields: 'Champs', + accountHasMoved: '{account} a déménagé', + profilePageForAccount: 'Page de profil pour {account}', + // About page + about: 'Infos', + aboutApp: 'Infos sur Pinafore', + aboutAppDescription: ` + <p> + Pinafore est un logiciel + <a rel="noopener" target="_blank" + href="https://github.com/nolanlawson/pinafore">gratuit et open-source</a> + créé par + <a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a> + et distribué sous le + <a rel="noopener" target="_blank" + href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">License GNU Affero General Public (AGPL)</a>. + </p> + + <h2 id="privacy-policy">Politique de confidentialité</h2> + + <p> + Pinafore ne garde pas d'informations personelles dans ses serveurs, + y compris les noms, addresses courriel, addresses IP, messages, et photos. + </p> + + <p> + Pinafore est un site statique. Tous données sont gardées en locale dans le navigateur, et sont partagée qu'avec + les instances auxquelles vous vous connectez. + </p> + + <h2>Crédits</h2> + + <p> + Icônes par <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>. + </p> + + <p> + Logo grâce à Gregor Cresnar du + <a rel="noopener" target="_blank" href="https://thenounproject.com/">Noun Project</a>. + </p>`, + // Settings + settings: 'Paramètres', + general: 'Général', + generalSettings: 'Paramètres générales', + showSensitive: 'Afficher les medias sensible par défaut', + showPlain: 'Afficher un simple gris pour les medias sensibles', + allSensitive: 'Considérer tous medias comme sensible', + largeMedia: 'Afficher de plus grands images et vidéos', + autoplayGifs: 'Repasser automatiquement les GIFs animés', + hideCards: 'Cacher les liens «cartes»', + underlineLinks: 'Souligner les liens dans les pouets et profils', + accessibility: 'Accessibilité', + reduceMotion: 'Reduire la motions dans les animations', + disableTappable: "Désactiver l'espace touchable sur un pouet entier", + removeEmoji: "Enlever les emojis des noms d'utilisateur", + shortAria: 'Utiliser des etiquettes courtes ARIA', + theme: 'Thème', + themeForInstance: 'Theème pour {instance}', + disableCustomScrollbars: 'Désactiver les scrollbars customisés', + preferences: 'Préférences', + hotkeySettings: 'Paramètres de raccourcis clavier', + disableHotkeys: 'Désactiver les raccourcis clavier', + leftRightArrows: 'Les flèches gauche/droit change de focus plutôt que les pages', + guide: 'Guide', + reload: 'Recharger', + // Wellness settings + wellness: 'Bien-être', + wellnessSettings: 'Paramètres de bien-être', + wellnessDescription: `Les paramètres de bien-être sont dessinées pour rédruire les effets accrochants ou d'anxiété des réseaux sociaux. + Veuillez choisir les options qui marchent pour vous.`, + enableAll: 'Activer tous', + metrics: 'Métrics', + hideFollowerCount: 'Cacher le nombre de suivants (10 maximum)', + hideReblogCount: 'Cacher le nombre de partages', + hideFavoriteCount: 'Cacher le nombre de favoris', + hideUnread: "Cacher le nombre de notifications (c'est-à-dire le point rouge)", + ui: 'Interface Utilisateur', + grayscaleMode: 'Mode echelle de gris', + wellnessFooter: `Ces paramètres sont basé sur les recommendations du + <a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`, + // This is a link: "You can filter or disable notifications in the _instance settings_" + filterNotificationsPre: 'Vous pouvez filtrer ou désactiver les notifications dans les', + filterNotificationsText: "paramètres d'instance", + filterNotificationsPost: '', + // Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_ + // to see a description. It's hard to properly internationalize, so we just break up the strings. + disableInfiniteScrollPre: 'Désactiver le', + disableInfiniteScrollText: 'défilage infini', + disableInfiniteScrollDescription: `Quand le défilage infini est désactivé, les pouets nouveau ne + s'apparêtront pas automatique au haut ou au bas de l'historique. Plutôt, il y aura des boutons pour + charger sur demande.`, + disableInfiniteScrollPost: '', + // Instance settings + loggedInAs: 'Connecté en tant que', + homeTimelineFilters: "Filtres d'historique de l'acceuil", + notificationFilters: 'Filtres de notifications', + pushNotifications: 'Filtres de notifications push', + // Add instance page + storageError: `Il semble que Pinafore ne peut pas stocker les données en locale. Est-ce que votre navigateur + est en mode privé, ou est-ce qu'il bloque les cookies? Pinafore garde tous ses données en locale et + ne peut pas fonctionner sans LocalStorage ou IndexedDB.`, + javaScriptError: 'Le JavaScript devrait être activé pour continuer.', + enterInstanceName: "Saisir le nom d'instance", + instanceColon: 'Instance:', + // Custom tooltip, concatenated together + getAnInstancePre: "N'avez-vous pas d'", + getAnInstanceText: 'instance', + getAnInstanceDescription: 'Une instance est votre serveur Mastodon, par exemple mastodon.social ou cybre.space.', + getAnInstancePost: '?', + joinMastodon: 'Joignez-vous à Mastodon!', + instancesYouveLoggedInTo: 'Instances conntectées:', + addAnotherInstance: 'Ajouter une nouvelle instance', + youreNotLoggedIn: 'Vous êtes connecté(e) à aucune instance.', + currentInstanceLabel: `{instance} {current, select, + true {(instance actuelle)} + other {} + }`, + // Link text + logInToAnInstancePre: '', + logInToAnInstanceText: 'Se connecter à une instance', + logInToAnInstancePost: 'pour utiliser Pinafore.', + // Another custom tooltip + showRingPre: 'Afficher toujours', + showRingText: "l'anneau de focus", + showRingDescription: `L'anneau de focus est le contour qui indique l'élément en focus actuel. Par défaut, ce n'est + affiché que quand on utilise le clavier (et ne pas la souris ou l'écran touche), mais vous pouvez choisr de + l'afficher toujours.`, + showRingPost: '', + instances: 'Les instances', + addInstance: 'Ajouter une instance', + homeTimelineFilterSettings: "Paramètres de filtre d'historique", + showReblogs: 'Afficher les partages', + showReplies: 'Afficher les réponses', + switchOrLogOut: 'Changer ou se déconnecter de cette instance', + switchTo: "Changer d'instance à celle-ci", + switchToInstance: "Changer d'instance", + switchToNameOfInstance: "Faire {instance} l'instance actuelle", + logOut: 'Se déconnecter', + logOutOfInstanceConfirm: 'Déconnectez-vous de {instance}?', + notificationFilterSettings: 'Paramètres de filtre de notifications', + // Push notifications + browserDoesNotSupportPush: 'Votre navigateur ne soutient pas les notifications push.', + deniedPush: 'Vous avez désactivé les notifications push.', + pushNotificationsNote: 'Veuillez noter que les notifications push ne peuvent être activées que pour une instance à la fois.', + pushSettings: 'Paramètres de notifications push', + newFollowers: 'Suivants nouveaux', + reblogs: 'Partages', + pollResults: "Résultats d'enquête", + needToReauthenticate: 'Vous devez ré-authentiquer pour activer les notifications push. Déconnectez-vous de {instance}?', + failedToUpdatePush: 'Impossible de mettre à jour les paramètres de notifications push: {error}', + // Themes + chooseTheme: 'Choisir une thème', + darkBackground: 'Sombre', + lightBackground: 'Clair', + themeLabel: `{label} {default, select, + true {(défaut)} + other {} + }`, + animatedImage: 'Image animée: {description}', + showImage: `Afficher l'image {animated, select, + true {animée} + other {} + }: {description}`, + playVideoOrAudio: `Repasser {audio, select, + true {l'audio} + other {la vidéo} + }: {description}`, + accountFollowedYou: '{name} vous a suivi(e), {account}', + reblogCountsHidden: 'Nombre de partages caché', + favoriteCountsHidden: 'nombre de mises en favori caché', + rebloggedTimes: `Partagé {count, plural, + one {une fois} + other {{count} fois} + }`, + favoritedTimes: `Mis en favori {count, plural, + one {une fois} + other {{count} fois} + }`, + pinnedStatus: 'Pouet épinglé', + rebloggedYou: 'a partagé votre pouet', + favoritedYou: 'a mis en favori votre pouet', + followedYou: 'followed you', + pollYouCreatedEnded: 'Une enquête vous avez créée a terminée', + pollYouVotedEnded: 'Une enquête dans laquelle vous avez voté a terminée', + reblogged: 'partagé', + showSensitiveMedia: 'Afficher la média sensible', + hideSensitiveMedia: 'Cacher la média sensible', + clickToShowSensitive: 'Image sensible. Cliquer pour afficher.', + longPost: 'Pouet long', + // Accessible status labels + accountRebloggedYou: '{account} a partagé votre pouet', + accountFavoritedYou: '{account} a mis votre pouet en favori', + rebloggedByAccount: 'Partagé par {account}', + contentWarningContent: 'Avertissement: {spoiler}', + hasMedia: 'média', + hasPoll: 'enquête', + shortStatusLabel: 'Pouet {privacy} par {account}', + // Privacy types + public: 'Publique', + unlisted: 'Non listé', + followersOnly: 'Abonnés/abonnées uniquement', + direct: 'Direct', + // Themes + themeRoyal: 'Royale', + themeScarlet: 'Ecarlate', + themeSeafoam: 'Ecume', + themeHotpants: 'Hotpants', + themeOaken: 'Chêne', + themeMajesty: 'Majesté', + themeGecko: 'Gecko', + themeGrayscale: 'Echelle gris', + themeOzark: 'Ozark', + themeCobalt: 'Cobalt', + themeSorcery: 'Sorcellerie', + themePunk: 'Punk', + themeRiot: 'Riot', + themeHacker: 'Hacker', + themeMastodon: 'Mastodon', + themePitchBlack: 'Noir complet', + themeDarkGrayscale: 'Echelle gris sombre', + // Polls + voteOnPoll: 'Voter dans cette enquête', + pollChoices: 'Choix', + vote: 'Voter', + pollDetails: 'Détails', + refresh: 'Recharger', + expires: 'Se termine', + expired: 'Terminée', + voteCount: `{count, plural, + one {1 vote} + other {{count} votes} + }`, + // Status interactions + clickToShowThread: '{time} - cliquer pour afficher le discussion', + showMore: 'Afficher plus', + showLess: 'Afficher moins', + closeReply: 'Fermer la réponse', + cannotReblogFollowersOnly: "Impossible de partager car ce pouet n'est que pour les abonné(e)s", + cannotReblogDirectMessage: 'Impossible de partager car ce pouet est direct', + reblog: 'Partager', + reply: 'Répondre', + replyToThread: 'Répondre au discussion', + favorite: 'Mettre en favori', + unfavorite: 'Ne plus mettre en favori', + // timeline + loadingMore: 'Chargement en cours…', + loadMore: 'Charger plus', + showCountMore: 'Afficher {count} de plus', + nothingToShow: 'Rien à afficher.', + // status thread page + statusThreadPage: 'Page de discussion', + status: 'Pouet', + // toast messages + blockedAccount: 'Compte bloqué', + unblockedAccount: 'Compte ne plus bloqué', + unableToBlock: 'Impossible de bloquer ce compte: {error}', + unableToUnblock: 'Impossible de ne plus bloquer ce compte: {error}', + bookmarkedStatus: 'Ajouté aux signets', + unbookmarkedStatus: 'Enlever des signets', + unableToBookmark: "Impossible d'ajouter aux signets: {error}", + unableToUnbookmark: "Impossible d'enlever des signets: {error}", + cannotPostOffline: 'Vous ne pouvez pas poueter car vous êtes hors connexion', + unableToPost: 'Impossible de poueter: {error}', + statusDeleted: 'Pouet supprimé', + unableToDelete: 'Impossible de supprimer: {error}', + cannotFavoriteOffline: 'Vous ne pouvez pas mettre en favori car vous êtes hors connexion', + cannotUnfavoriteOffline: 'Vous ne pouvez pas enlever des favoris car vous êtes hors connexion', + unableToFavorite: 'Impossible de mettre en favori: {error}', + unableToUnfavorite: "Impossible d'enlever des favoris: {error}", + followedAccount: 'Compte suivi', + unfollowedAccount: 'Compte ne plus suivi', + unableToFollow: 'Impossible de suivre: {error}', + unableToUnfollow: 'Impossible de ne plus suivre: {error}', + accessTokenRevoked: 'Authentication revoquée, déconnecté de {instance}', + loggedOutOfInstance: 'Déconnecté de {instance}', + failedToUploadMedia: "Impossible d'uploader: {error}", + mutedAccount: 'Compte mis en sourdine', + unmutedAccount: 'Compte ne plus mis en sourdine', + unableToMute: 'Impossible de mettre en sourdine: {error}', + unableToUnmute: 'Impossible de plus mettre en sourdine: {error}', + mutedConversation: 'Conversation mis en sourdine', + unmutedConversation: 'Conversation ne plus mis en sourdine', + unableToMuteConversation: 'Impossible de mettre en sourdine: {error}', + unableToUnmuteConversation: 'Impossible de ne plus mettre en sourdine: {error}', + unpinnedStatus: 'Pouet ne plus épinglé', + unableToPinStatus: "Impossible d'épingler: {error}", + unableToUnpinStatus: 'Impossible de ne plus épingler: {error}', + unableToRefreshPoll: 'Impossible de recharger: {error}', + unableToVoteInPoll: 'Impossible de voter: {error}', + cannotReblogOffline: 'Vous ne pouvez pas partager car vous êtes hors de connexion.', + cannotUnreblogOffline: 'Vous ne pouvez pas ne plus partager car vous êtes hors de connexion.', + failedToReblog: 'Impossible de partager: {error}', + failedToUnreblog: 'Impossible de ne plus partager: {error}', + submittedReport: 'Report signalé', + failedToReport: 'Impossible de signaler: {error}', + approvedFollowRequest: 'Demande de suivre approuvée', + rejectedFollowRequest: 'Demande de suivre rejetée', + unableToApproveFollowRequest: "Impossible d'appouver: {error}", + unableToRejectFollowRequest: 'Impossible de rejeter: {error}', + searchError: 'Erreur de recherche: {error}', + hidDomain: 'Domaine cachée', + unhidDomain: 'Domaine ne plus cachée', + unableToHideDomain: 'Impossible de cacher la domaine: {error}', + unableToUnhideDomain: 'Imipossible de ne plus cacher la domaine: {error}', + showingReblogs: 'Partages affichés', + hidingReblogs: 'Partages ne plus affichés', + unableToShowReblogs: "Impossible d'afficher les partages: {error}", + unableToHideReblogs: 'Impossible de ne plus afficher les partages: {error}', + unableToShare: 'Impossible de partager externellement: {error}', + showingOfflineContent: "Requête d'internet impossible. Contenu hors de connexion affiché.", + youAreOffline: 'Il semble que vous êtes hors de connextion. Vous pouvez toujours lire les pouets dans cet état.', + // Snackbar UI + updateAvailable: 'Mise à jour disponible.' +} diff --git a/src/routes/_a11y/getAccessibleLabelForStatus.js b/src/routes/_a11y/getAccessibleLabelForStatus.js index 05f3ae8b..2fa78bb3 100644 --- a/src/routes/_a11y/getAccessibleLabelForStatus.js +++ b/src/routes/_a11y/getAccessibleLabelForStatus.js @@ -1,5 +1,6 @@ import { getAccountAccessibleName } from './getAccountAccessibleName' import { POST_PRIVACY_OPTIONS } from '../_static/statuses' +import { formatIntl } from '../_utils/formatIntl' function getNotificationText (notification, omitEmojiInDisplayNames) { if (!notification) { @@ -7,9 +8,9 @@ function getNotificationText (notification, omitEmojiInDisplayNames) { } const notificationAccountDisplayName = getAccountAccessibleName(notification.account, omitEmojiInDisplayNames) if (notification.type === 'reblog') { - return `${notificationAccountDisplayName} boosted your status` + return formatIntl('intl.accountRebloggedYou', { account: notificationAccountDisplayName }) } else if (notification.type === 'favourite') { - return `${notificationAccountDisplayName} favorited your status` + return formatIntl('intl.accountFavoritedYou', { account: notificationAccountDisplayName }) } } @@ -26,7 +27,7 @@ function getReblogText (reblog, account, omitEmojiInDisplayNames) { return } const accountDisplayName = getAccountAccessibleName(account, omitEmojiInDisplayNames) - return `Boosted by ${accountDisplayName}` + return formatIntl('intl.rebloggedByAccount', { account: accountDisplayName }) } function cleanupText (text) { @@ -40,15 +41,15 @@ export function getAccessibleLabelForStatus (originalAccount, account, plainText const originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames) const contentTextToShow = (showContent || !spoilerText) ? cleanupText(plainTextContent) - : `Content warning: ${cleanupText(spoilerText)}` - const mediaTextToShow = showMedia && 'has media' - const pollTextToShow = showPoll && 'has poll' + : formatIntl('intl.contentWarningContent', { spoiler: cleanupText(spoilerText) }) + const mediaTextToShow = showMedia && 'intl.hasMedia' + const pollTextToShow = showPoll && 'intl.hasPoll' const privacyText = getPrivacyText(visibility) if (disableLongAriaLabels) { // Long text can crash NVDA; allow users to shorten it like we had it before. // https://github.com/nolanlawson/pinafore/issues/694 - return `${privacyText} status by ${originalAccountDisplayName}` + return formatIntl('intl.shortStatusLabel', { privacy: privacyText, account: originalAccountDisplayName }) } const values = [ diff --git a/src/routes/_actions/block.js b/src/routes/_actions/block.js index a0efee1c..10409f61 100644 --- a/src/routes/_actions/block.js +++ b/src/routes/_actions/block.js @@ -3,6 +3,7 @@ import { blockAccount, unblockAccount } from '../_api/block' import { toast } from '../_components/toast/toast' import { updateLocalRelationship } from './accounts' import { emit } from '../_utils/eventBus' +import { formatIntl } from '../_utils/formatIntl' export async function setAccountBlocked (accountId, block, toastOnSuccess) { const { currentInstance, accessToken } = store.get() @@ -16,14 +17,17 @@ export async function setAccountBlocked (accountId, block, toastOnSuccess) { await updateLocalRelationship(currentInstance, accountId, relationship) if (toastOnSuccess) { if (block) { - toast.say('Blocked account') + /* no await */ toast.say('intl.blockedAccount') } else { - toast.say('Unblocked account') + /* no await */ toast.say('intl.unblockedAccount') } } emit('refreshAccountsList') } catch (e) { console.error(e) - toast.say(`Unable to ${block ? 'block' : 'unblock'} account: ` + (e.message || '')) + /* no await */ toast.say(block + ? formatIntl('intl.unableToBlock', { block: !!block, error: (e.message || '') }) + : formatIntl('intl.unableToUnblock', { error: (e.message || '') }) + ) } } diff --git a/src/routes/_actions/bookmark.js b/src/routes/_actions/bookmark.js index ac52f620..3759c03c 100644 --- a/src/routes/_actions/bookmark.js +++ b/src/routes/_actions/bookmark.js @@ -2,6 +2,7 @@ import { store } from '../_store/store' import { toast } from '../_components/toast/toast' import { bookmarkStatus, unbookmarkStatus } from '../_api/bookmark' import { database } from '../_database/database' +import { formatIntl } from '../_utils/formatIntl' export async function setStatusBookmarkedOrUnbookmarked (statusId, bookmarked) { const { currentInstance, accessToken } = store.get() @@ -12,14 +13,18 @@ export async function setStatusBookmarkedOrUnbookmarked (statusId, bookmarked) { await unbookmarkStatus(currentInstance, accessToken, statusId) } if (bookmarked) { - toast.say('Bookmarked toot') + /* no await */ toast.say('intl.bookmarkedStatus') } else { - toast.say('Unbookmarked toot') + /* no await */ toast.say('intl.unbookmarkedStatus') } store.setStatusBookmarked(currentInstance, statusId, bookmarked) await database.setStatusBookmarked(currentInstance, statusId, bookmarked) } catch (e) { console.error(e) - toast.say(`Unable to ${bookmarked ? 'bookmark' : 'unbookmark'} toot: ` + (e.message || '')) + /* no await */toast.say( + bookmarked + ? formatIntl('intl.unableToBookmark', { error: (e.message || '') }) + : formatIntl('intl.unableToUnbookmark', { error: (e.message || '') }) + ) } } diff --git a/src/routes/_actions/compose.js b/src/routes/_actions/compose.js index aa17480a..f7d74164 100644 --- a/src/routes/_actions/compose.js +++ b/src/routes/_actions/compose.js @@ -8,6 +8,7 @@ import { putMediaMetadata } from '../_api/media' import uniqBy from 'lodash-es/uniqBy' import { deleteCachedMediaFile } from '../_utils/mediaUploadFileCache' import { scheduleIdleTask } from '../_utils/scheduleIdleTask' +import { formatIntl } from '../_utils/formatIntl' export async function insertHandleForReply (statusId) { const { currentInstance } = store.get() @@ -31,7 +32,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds, const { currentInstance, accessToken, online } = store.get() if (!online) { - toast.say('You cannot post while offline') + /* no await */ toast.say('intl.cannotPostOffline') return } @@ -63,7 +64,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds, scheduleIdleTask(() => (mediaIds || []).forEach(mediaId => deleteCachedMediaFile(mediaId))) // clean up media cache } catch (e) { console.error(e) - toast.say('Unable to post status: ' + (e.message || '')) + /* no await */ toast.say(formatIntl('intl.unableToPost', { error: (e.message || '') })) } finally { store.set({ postingStatus: false }) } diff --git a/src/routes/_actions/copyText.js b/src/routes/_actions/copyText.js index 96416e4c..4b3fcb0e 100644 --- a/src/routes/_actions/copyText.js +++ b/src/routes/_actions/copyText.js @@ -5,7 +5,7 @@ export async function copyText (text) { if (navigator.clipboard) { // not supported in all browsers try { await navigator.clipboard.writeText(text) - toast.say('Copied to clipboard') + /* no await */ toast.say('intl.copiedToClipboard') return } catch (e) { console.error(e) diff --git a/src/routes/_actions/delete.js b/src/routes/_actions/delete.js index 9c0c9b2e..8e4a260a 100644 --- a/src/routes/_actions/delete.js +++ b/src/routes/_actions/delete.js @@ -2,17 +2,18 @@ import { store } from '../_store/store' import { deleteStatus } from '../_api/delete' import { toast } from '../_components/toast/toast' import { deleteStatus as deleteStatusLocally } from './deleteStatuses' +import { formatIntl } from '../_utils/formatIntl' export async function doDeleteStatus (statusId) { const { currentInstance, accessToken } = store.get() try { const deletedStatus = await deleteStatus(currentInstance, accessToken, statusId) deleteStatusLocally(currentInstance, statusId) - toast.say('Status deleted.') + /* no await */ toast.say('intl.statusDeleted') return deletedStatus } catch (e) { console.error(e) - toast.say('Unable to delete status: ' + (e.message || '')) + /* no await */ toast.say(formatIntl('intl.unableToDelete', { error: (e.message || '') })) throw e } } diff --git a/src/routes/_actions/favorite.js b/src/routes/_actions/favorite.js index b2953328..c8d43958 100644 --- a/src/routes/_actions/favorite.js +++ b/src/routes/_actions/favorite.js @@ -2,11 +2,12 @@ import { favoriteStatus, unfavoriteStatus } from '../_api/favorite' import { store } from '../_store/store' import { toast } from '../_components/toast/toast' import { database } from '../_database/database' +import { formatIntl } from '../_utils/formatIntl' export async function setFavorited (statusId, favorited) { const { online } = store.get() if (!online) { - toast.say(`You cannot ${favorited ? 'favorite' : 'unfavorite'} while offline.`) + /* no await */ toast.say(favorited ? 'intl.cannotFavoriteOffline' : 'intl.cannotUnfavoriteOffline') return } const { currentInstance, accessToken } = store.get() @@ -19,7 +20,10 @@ export async function setFavorited (statusId, favorited) { await database.setStatusFavorited(currentInstance, statusId, favorited) } catch (e) { console.error(e) - toast.say(`Failed to ${favorited ? 'favorite' : 'unfavorite'}. ` + (e.message || '')) + /* no await */ toast.say(favorited + ? formatIntl('intl.unableToFavorite', { error: (e.message || '') }) + : formatIntl('intl.unableToUnfavorite', { error: (e.message || '') }) + ) store.setStatusFavorited(currentInstance, statusId, !favorited) // undo optimistic update } } diff --git a/src/routes/_actions/follow.js b/src/routes/_actions/follow.js index 668a94c2..e1eed3c0 100644 --- a/src/routes/_actions/follow.js +++ b/src/routes/_actions/follow.js @@ -2,6 +2,7 @@ import { store } from '../_store/store' import { followAccount, unfollowAccount } from '../_api/follow' import { toast } from '../_components/toast/toast' import { updateLocalRelationship } from './accounts' +import { formatIntl } from '../_utils/formatIntl' export async function setAccountFollowed (accountId, follow, toastOnSuccess) { const { currentInstance, accessToken } = store.get() @@ -14,14 +15,13 @@ export async function setAccountFollowed (accountId, follow, toastOnSuccess) { } await updateLocalRelationship(currentInstance, accountId, relationship) if (toastOnSuccess) { - if (follow) { - toast.say('Followed account') - } else { - toast.say('Unfollowed account') - } + /* no await */ toast.say(follow ? 'intl.followedAccount' : 'intl.unfollowedAccount') } } catch (e) { console.error(e) - toast.say(`Unable to ${follow ? 'follow' : 'unfollow'} account: ` + (e.message || '')) + /* no await */ toast.say(follow + ? formatIntl('intl.unableToFollow', { error: (e.message || '') }) + : formatIntl('intl.unableToUnfollow', { error: (e.message || '') }) + ) } } diff --git a/src/routes/_actions/instances.js b/src/routes/_actions/instances.js index 5701ee11..a1812af8 100644 --- a/src/routes/_actions/instances.js +++ b/src/routes/_actions/instances.js @@ -7,6 +7,7 @@ import { cacheFirstUpdateAfter } from '../_utils/sync' import { getInstanceInfo } from '../_api/instance' import { database } from '../_database/database' import { importVirtualListStore } from '../_utils/asyncModules/importVirtualListStore.js' +import { formatIntl } from '../_utils/formatIntl' export function changeTheme (instanceName, newTheme) { const { instanceThemes } = store.get() @@ -32,7 +33,8 @@ export function switchToInstance (instanceName) { switchToTheme(instanceThemes[instanceName], enableGrayscale) } -export async function logOutOfInstance (instanceName, message = `Logged out of ${instanceName}`) { +export async function logOutOfInstance (instanceName, message) { + message = message || formatIntl('intl.loggedOutOfInstance', { instance: instanceName }) const { composeData, currentInstance, @@ -123,7 +125,7 @@ export async function updateInstanceInfo (instanceName) { export function logOutOnUnauthorized (instanceName) { return async error => { if (error.message.startsWith('401:')) { - await logOutOfInstance(instanceName, `The access token was revoked, logged out of ${instanceName}`) + await logOutOfInstance(instanceName, formatIntl('intl.accessTokenRevoked', { instance: instanceName })) } throw error diff --git a/src/routes/_actions/media.js b/src/routes/_actions/media.js index d3638829..5b4beddd 100644 --- a/src/routes/_actions/media.js +++ b/src/routes/_actions/media.js @@ -25,7 +25,7 @@ export async function doMediaUpload (realm, file) { scheduleIdleTask(() => store.save()) } catch (e) { console.error(e) - toast.say('Failed to upload media: ' + (e.message || '')) + /* no await */ toast.say('intl.failedToUploadMedia', { error: (e.message || '') }) } finally { store.set({ uploadingMedia: false }) } diff --git a/src/routes/_actions/mute.js b/src/routes/_actions/mute.js index 6d34cb69..d4bbe531 100644 --- a/src/routes/_actions/mute.js +++ b/src/routes/_actions/mute.js @@ -3,6 +3,7 @@ import { muteAccount, unmuteAccount } from '../_api/mute' import { toast } from '../_components/toast/toast' import { updateLocalRelationship } from './accounts' import { emit } from '../_utils/eventBus' +import { formatIntl } from '../_utils/formatIntl' export async function setAccountMuted (accountId, mute, notifications, toastOnSuccess) { const { currentInstance, accessToken } = store.get() @@ -15,15 +16,14 @@ export async function setAccountMuted (accountId, mute, notifications, toastOnSu } await updateLocalRelationship(currentInstance, accountId, relationship) if (toastOnSuccess) { - if (mute) { - toast.say('Muted account') - } else { - toast.say('Unmuted account') - } + /* no await */ toast.say(mute ? 'intl.mutedAccount' : 'intl.unmutedAccount') } emit('refreshAccountsList') } catch (e) { console.error(e) - toast.say(`Unable to ${mute ? 'mute' : 'unmute'} account: ` + (e.message || '')) + /* no await */ toast.say(mute + ? formatIntl('intl.unableToMute', { error: (e.message || '') }) + : formatIntl('intl.unableToUnmute', { error: (e.message || '') }) + ) } } diff --git a/src/routes/_actions/muteConversation.js b/src/routes/_actions/muteConversation.js index 620a2e34..2b3df5b2 100644 --- a/src/routes/_actions/muteConversation.js +++ b/src/routes/_actions/muteConversation.js @@ -2,6 +2,7 @@ import { store } from '../_store/store' import { muteConversation, unmuteConversation } from '../_api/muteConversation' import { toast } from '../_components/toast/toast' import { database } from '../_database/database' +import { formatIntl } from '../_utils/formatIntl' export async function setConversationMuted (statusId, mute, toastOnSuccess) { const { currentInstance, accessToken } = store.get() @@ -13,14 +14,13 @@ export async function setConversationMuted (statusId, mute, toastOnSuccess) { } await database.setStatusMuted(currentInstance, statusId, mute) if (toastOnSuccess) { - if (mute) { - toast.say('Muted conversation') - } else { - toast.say('Unmuted conversation') - } + /* no await */ toast.say(mute ? 'intl.mutedConversation' : 'intl.unmutedConversation') } } catch (e) { console.error(e) - toast.say(`Unable to ${mute ? 'mute' : 'unmute'} conversation: ` + (e.message || '')) + /* no await */ toast.say(mute + ? formatIntl('intl.unableToMuteConversation', { error: (e.message || '') }) + : formatIntl('intl.unableToUnmuteConversation', { error: (e.message || '') }) + ) } } diff --git a/src/routes/_actions/pin.js b/src/routes/_actions/pin.js index 94137645..3d44afc0 100644 --- a/src/routes/_actions/pin.js +++ b/src/routes/_actions/pin.js @@ -3,6 +3,7 @@ import { toast } from '../_components/toast/toast' import { pinStatus, unpinStatus } from '../_api/pin' import { database } from '../_database/database' import { emit } from '../_utils/eventBus' +import { formatIntl } from '../_utils/formatIntl' export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) { const { currentInstance, accessToken } = store.get() @@ -13,17 +14,16 @@ export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSucces await unpinStatus(currentInstance, accessToken, statusId) } if (toastOnSuccess) { - if (pinned) { - toast.say('Pinned status') - } else { - toast.say('Unpinned status') - } + /* no await */ toast.say(pinned ? 'intl.pinnedStatus' : 'intl.unpinnedStatus') } store.setStatusPinned(currentInstance, statusId, pinned) await database.setStatusPinned(currentInstance, statusId, pinned) emit('updatePinnedStatuses') } catch (e) { console.error(e) - toast.say(`Unable to ${pinned ? 'pin' : 'unpin'} status: ` + (e.message || '')) + /* no await */ toast.say(pinned + ? formatIntl('intl.unableToPinStatus', { error: (e.message || '') }) + : formatIntl('intl.unableToUnpinStatus', { error: (e.message || '') }) + ) } } diff --git a/src/routes/_actions/polls.js b/src/routes/_actions/polls.js index 5e532c44..aba6173a 100644 --- a/src/routes/_actions/polls.js +++ b/src/routes/_actions/polls.js @@ -1,6 +1,7 @@ import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls' import { store } from '../_store/store' import { toast } from '../_components/toast/toast' +import { formatIntl } from '../_utils/formatIntl' export async function getPoll (pollId) { const { currentInstance, accessToken } = store.get() @@ -9,7 +10,7 @@ export async function getPoll (pollId) { return poll } catch (e) { console.error(e) - toast.say('Unable to refresh poll: ' + (e.message || '')) + /* no await */ toast.say(formatIntl('intl.unableToRefreshPoll', { error: (e.message || '') })) } } @@ -20,6 +21,6 @@ export async function voteOnPoll (pollId, choices) { return poll } catch (e) { console.error(e) - toast.say('Unable to vote in poll: ' + (e.message || '')) + /* no await */ toast.say(formatIntl('intl.unableToVoteInPoll', { error: (e.message || '') })) } } diff --git a/src/routes/_actions/reblog.js b/src/routes/_actions/reblog.js index 87b9f007..ba9a24d9 100644 --- a/src/routes/_actions/reblog.js +++ b/src/routes/_actions/reblog.js @@ -2,11 +2,12 @@ import { store } from '../_store/store' import { toast } from '../_components/toast/toast' import { reblogStatus, unreblogStatus } from '../_api/reblog' import { database } from '../_database/database' +import { formatIntl } from '../_utils/formatIntl' export async function setReblogged (statusId, reblogged) { const online = store.get() if (!online) { - toast.say(`You cannot ${reblogged ? 'boost' : 'unboost'} while offline.`) + /* no await */ toast.say(reblogged ? 'intl.cannotReblogOffline' : 'intl.cannotUnreblogOffline') return } const { currentInstance, accessToken } = store.get() @@ -19,7 +20,10 @@ export async function setReblogged (statusId, reblogged) { await database.setStatusReblogged(currentInstance, statusId, reblogged) } catch (e) { console.error(e) - toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || '')) + /* no await */ toast.say(reblogged + ? formatIntl('intl.failedToReblog', { error: (e.message || '') }) + : formatIntl('intl.failedToUnreblog', { error: (e.message || '') }) + ) store.setStatusReblogged(currentInstance, statusId, !reblogged) // undo optimistic update } } diff --git a/src/routes/_actions/reportStatuses.js b/src/routes/_actions/reportStatuses.js index 46806024..67e6c76f 100644 --- a/src/routes/_actions/reportStatuses.js +++ b/src/routes/_actions/reportStatuses.js @@ -1,13 +1,14 @@ import { store } from '../_store/store' import { toast } from '../_components/toast/toast' import { report } from '../_api/report' +import { formatIntl } from '../_utils/formatIntl' export async function reportStatuses (account, statusIds, comment, forward) { const { currentInstance, accessToken } = store.get() try { await report(currentInstance, accessToken, account.id, statusIds, comment, forward) - toast.say('Submitted report') + /* no await */ toast.say('intl.submittedReport') } catch (e) { - toast.say('Failed to report: ' + (e.message || '')) + /* no await */ toast.say(formatIntl('intl.failedToReport', { error: (e.message || '') })) } } diff --git a/src/routes/_actions/requests.js b/src/routes/_actions/requests.js index 2d46668d..ad763ade 100644 --- a/src/routes/_actions/requests.js +++ b/src/routes/_actions/requests.js @@ -2,6 +2,7 @@ import { store } from '../_store/store' import { approveFollowRequest, rejectFollowRequest } from '../_api/requests' import { emit } from '../_utils/eventBus' import { toast } from '../_components/toast/toast' +import { formatIntl } from '../_utils/formatIntl' export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) { const { @@ -15,15 +16,14 @@ export async function setFollowRequestApprovedOrRejected (accountId, approved, t await rejectFollowRequest(currentInstance, accessToken, accountId) } if (toastOnSuccess) { - if (approved) { - toast.say('Approved follow request') - } else { - toast.say('Rejected follow request') - } + /* no await */ toast.say(approved ? 'intl.approvedFollowRequest' : 'intl.rejectedFollowRequest') } emit('refreshAccountsList') } catch (e) { console.error(e) - toast.say(`Unable to ${approved ? 'approve' : 'reject'} account: ` + (e.message || '')) + /* no await */ toast.say(approved + ? formatIntl('intl.unableToApproveFollowRequest', { error: (e.message || '') }) + : formatIntl('intl.unableToRejectFollowRequest', { error: (e.message || '') }) + ) } } diff --git a/src/routes/_actions/search.js b/src/routes/_actions/search.js index 13a30e03..43b057ad 100644 --- a/src/routes/_actions/search.js +++ b/src/routes/_actions/search.js @@ -1,6 +1,7 @@ import { store } from '../_store/store' import { toast } from '../_components/toast/toast' import { search } from '../_api/search' +import { formatIntl } from '../_utils/formatIntl' export async function doSearch () { const { currentInstance, accessToken, queryInSearch } = store.get() @@ -15,7 +16,7 @@ export async function doSearch () { }) } } catch (e) { - toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || '')) + /* no await */ toast.say(formatIntl('intl.searchError', { error: (e.message || '') })) console.error(e) } finally { store.set({ searchLoading: false }) diff --git a/src/routes/_actions/setDomainBlocked.js b/src/routes/_actions/setDomainBlocked.js index a9a635b0..ff5ac0ef 100644 --- a/src/routes/_actions/setDomainBlocked.js +++ b/src/routes/_actions/setDomainBlocked.js @@ -2,6 +2,7 @@ import { store } from '../_store/store' import { blockDomain, unblockDomain } from '../_api/blockDomain' import { toast } from '../_components/toast/toast' import { updateRelationship } from './accounts' +import { formatIntl } from '../_utils/formatIntl' export async function setDomainBlocked (accountId, domain, block, toastOnSuccess) { const { currentInstance, accessToken } = store.get() @@ -13,14 +14,13 @@ export async function setDomainBlocked (accountId, domain, block, toastOnSuccess } await updateRelationship(accountId) if (toastOnSuccess) { - if (block) { - toast.say(`Hiding ${domain}`) - } else { - toast.say(`Unhiding ${domain}`) - } + /* no await */ toast.say(block ? 'intl.hidDomain' : 'intl.unhidDomain') } } catch (e) { console.error(e) - toast.say(`Unable to ${block ? 'hide' : 'unhide'} domain: ` + (e.message || '')) + /* no await */ toast.say(block + ? formatIntl('intl.unableToHideDomain', { error: (e.message || '') }) + : formatIntl('intl.unableToUnhideDomain', { error: (e.message || '') }) + ) } } diff --git a/src/routes/_actions/setShowReblogs.js b/src/routes/_actions/setShowReblogs.js index c7cb0dc6..9f00afa1 100644 --- a/src/routes/_actions/setShowReblogs.js +++ b/src/routes/_actions/setShowReblogs.js @@ -2,6 +2,7 @@ import { store } from '../_store/store' import { setShowReblogs as setShowReblogsApi } from '../_api/showReblogs' import { toast } from '../_components/toast/toast' import { updateLocalRelationship } from './accounts' +import { formatIntl } from '../_utils/formatIntl' export async function setShowReblogs (accountId, showReblogs, toastOnSuccess) { const { currentInstance, accessToken } = store.get() @@ -9,14 +10,13 @@ export async function setShowReblogs (accountId, showReblogs, toastOnSuccess) { const relationship = await setShowReblogsApi(currentInstance, accessToken, accountId, showReblogs) await updateLocalRelationship(currentInstance, accountId, relationship) if (toastOnSuccess) { - if (showReblogs) { - toast.say('Showing boosts') - } else { - toast.say('Hiding boosts') - } + /* no await */ toast.say(showReblogs ? 'intl.showingReblogs' : 'intl.hidingReblogs') } } catch (e) { console.error(e) - toast.say(`Unable to ${showReblogs ? 'show' : 'hide'} boosts: ` + (e.message || '')) + /* no await */ toast.say(showReblogs + ? formatIntl('intl.unableToShowReblogs', { error: (e.message || '') }) + : formatIntl('intl.unableToHideReblogs', { error: (e.message || '') }) + ) } } diff --git a/src/routes/_actions/share.js b/src/routes/_actions/share.js index e60f8ba1..38cbdbfe 100644 --- a/src/routes/_actions/share.js +++ b/src/routes/_actions/share.js @@ -1,5 +1,6 @@ import { toast } from '../_components/toast/toast' import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText' +import { formatIntl } from '../_utils/formatIntl' export async function shareStatus (status) { try { @@ -9,6 +10,6 @@ export async function shareStatus (status) { url: status.url }) } catch (e) { - toast.say('Unable to share: ' + (e.message || '')) + /* no await */ toast.say(formatIntl('intl.unableToShare', { error: (e.message || '') })) } } diff --git a/src/routes/_actions/timeline.js b/src/routes/_actions/timeline.js index 7b409921..4efb7b1a 100644 --- a/src/routes/_actions/timeline.js +++ b/src/routes/_actions/timeline.js @@ -142,7 +142,7 @@ async function fetchTimelineItems (instanceName, accessToken, timelineName, onli await storeFreshTimelineItemsInDatabase(instanceName, timelineName, items) } catch (e) { console.error(e) - toast.say('Internet request failed. Showing offline content.') + /* no await */ toast.say('intl.showingOfflineContent') items = await database.getTimeline(instanceName, timelineName, lastTimelineItemId, TIMELINE_BATCH_SIZE) stale = true } diff --git a/src/routes/_components/AccountsListPage.html b/src/routes/_components/AccountsListPage.html index e70d1dbb..9179befe 100644 --- a/src/routes/_components/AccountsListPage.html +++ b/src/routes/_components/AccountsListPage.html @@ -36,6 +36,7 @@ import AccountSearchResult from './search/AccountSearchResult.html' import { toast } from './toast/toast' import { on } from '../_utils/eventBus' + import { formatIntl } from '../_utils/formatIntl' // TODO: paginate export default { @@ -43,7 +44,7 @@ try { await this.refreshAccounts() } catch (e) { - toast.say('Error: ' + (e.name || '') + ' ' + (e.message || '')) + /* no await */ toast.say(formatIntl('intl.error', { error: (e.message || '') })) } finally { this.set({ loading: false }) } diff --git a/src/routes/_components/DynamicPageBanner.html b/src/routes/_components/DynamicPageBanner.html index df111f55..8c768678 100644 --- a/src/routes/_components/DynamicPageBanner.html +++ b/src/routes/_components/DynamicPageBanner.html @@ -1,5 +1,5 @@ <div class="dynamic-page-banner {icon ? 'dynamic-page-with-icon' : ''}" - role="navigation" aria-label="Page header" + role="navigation" aria-label="{intl.pageHeader}" > {#if icon} <SvgIcon className="dynamic-page-banner-svg" href={icon} /> @@ -7,8 +7,8 @@ <h1 class="dynamic-page-title" aria-label={ariaTitle}>{title}</h1> <button type="button" class="dynamic-page-go-back" - aria-label="Go back" - on:click|preventDefault="onGoBack()">Back</button> + aria-label="{intl.goBack}" + on:click|preventDefault="onGoBack()">{intl.back}</button> </div> <Shortcut key="Backspace" on:pressed="onGoBack()"/> <style> diff --git a/src/routes/_components/InformationalFooter.html b/src/routes/_components/InformationalFooter.html index 5fef7497..9c629c93 100644 --- a/src/routes/_components/InformationalFooter.html +++ b/src/routes/_components/InformationalFooter.html @@ -1,18 +1,6 @@ <HiddenFromSSR> <footer> - <!-- Use raw HTML to make the output smaller --> - {@html ` - <p> - Pinafore is - <a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">open-source software</a> - created by - <a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a> - and distributed under the - <a rel="noopener" target="_blank" - href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">AGPL License</a>. - Here is the <a href="/settings/about#privacy-policy" rel="prefetch">privacy policy</a>. - </p> - `} + {@html intl.footer} </footer> </HiddenFromSSR> <script> diff --git a/src/routes/_components/LengthIndicator.html b/src/routes/_components/LengthIndicator.html index 01ef12cb..e6a389b2 100644 --- a/src/routes/_components/LengthIndicator.html +++ b/src/routes/_components/LengthIndicator.html @@ -17,6 +17,7 @@ import { store } from '../_store/store' import { observe } from 'svelte-extras' import { throttleTimer } from '../_utils/throttleTimer' + import { formatIntl } from '../_utils/formatIntl' const updateDisplayedLength = process.browser && throttleTimer(requestAnimationFrame) @@ -43,9 +44,9 @@ lengthToDisplay: ({ length, max }) => max - length, lengthLabel: ({ overLimit, lengthToDisplayDeferred }) => { if (overLimit) { - return `${-lengthToDisplayDeferred} characters over limit` + return formatIntl('intl.overLimit', { count: -lengthToDisplayDeferred }) } else { - return `${lengthToDisplayDeferred} characters remaining` + return formatIntl('intl.underLimit', { count: lengthToDisplayDeferred }) } } }, diff --git a/src/routes/_components/LoadingSpinner.html b/src/routes/_components/LoadingSpinner.html index aa56be2b..f9202038 100644 --- a/src/routes/_components/LoadingSpinner.html +++ b/src/routes/_components/LoadingSpinner.html @@ -1,7 +1,7 @@ <SvgIcon className="loading-spinner-icon spin {maskStyle ? 'mask-style' : ''}" style="width: {size}px; height: {size}px;" href="#fa-spinner" - ariaLabel="Loading" + ariaLabel="{intl.loading}" /> <style> :global(.loading-spinner-icon) { diff --git a/src/routes/_components/NavItem.html b/src/routes/_components/NavItem.html index 93f6e9ee..ff569d18 100644 --- a/src/routes/_components/NavItem.html +++ b/src/routes/_components/NavItem.html @@ -128,6 +128,7 @@ import { doubleRAF } from '../_utils/doubleRAF' import { scrollToTop } from '../_utils/scrollToTop' import { normalizePageName } from '../_utils/normalizePageName' + import { formatIntl } from '../_utils/formatIntl' export default { oncreate () { @@ -150,16 +151,15 @@ computed: { selected: ({ page, name }) => name === normalizePageName(page), ariaLabel: ({ selected, name, label, $numberOfNotifications, $numberOfFollowRequests }) => { - let res = label - if (selected) { - res += ' (current page)' - } - if (name === 'notifications' && $numberOfNotifications) { - res += ` (${$numberOfNotifications} notification${$numberOfNotifications === 1 ? '' : 's'})` - } else if (name === 'community' && $numberOfFollowRequests) { - res += ` (${$numberOfFollowRequests} follow request${$numberOfFollowRequests === 1 ? '' : 's'})` - } - return res + const count = name === 'notifications' + ? $numberOfNotifications + : (name === 'community' ? $numberOfFollowRequests : 0) + return formatIntl('intl.navItemLabel', { + label, + selected, + name, + count + }) }, showBadge: ({ name, $hasNotifications, $hasFollowRequests }) => ( (name === 'notifications' && $hasNotifications) || (name === 'community' && $hasFollowRequests) diff --git a/src/routes/_components/NotLoggedInHome.html b/src/routes/_components/NotLoggedInHome.html index fbbadebb..a835a314 100644 --- a/src/routes/_components/NotLoggedInHome.html +++ b/src/routes/_components/NotLoggedInHome.html @@ -3,30 +3,14 @@ <div class="not-logged-in-home"> <div class="banner"> <SvgIcon className="not-logged-in-home-svg" href="#pinafore-logo" /> - <h1>Pinafore</h1> + <h1>{intl.appName}</h1> + </div> + <div> + {@html intl.homeDescription} + <p style="text-align: right;"> + <a class="button primary" rel="prefetch" href="/settings/instances/add">{intl.logIn}</a> + </p> </div> - <!-- Use raw HTML to make the output smaller --> - {@html ` - <div> - <p> - Pinafore is a web client for - <a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>, - designed for speed and simplicity. - </p> - - <p> - Read the - <a rel="noopener" target="_blank" - href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">introductory blog post</a>, - or get started by logging in to an instance: - </p> - - <p style="text-align: right;"> - <a class="button primary" rel="prefetch" href="/settings/instances/add">Log in</a> - </p> - </div> - `} - </div> </FreeTextLayout> </HiddenFromSSR> <style> diff --git a/src/routes/_components/NotificationFilters.html b/src/routes/_components/NotificationFilters.html index a1f214e8..a182295a 100644 --- a/src/routes/_components/NotificationFilters.html +++ b/src/routes/_components/NotificationFilters.html @@ -1,5 +1,5 @@ <TabSet - label="Filters" + label="{intl.filters}" currentTabName={filter} {tabs} className="notification-filters" @@ -12,12 +12,12 @@ tabs: [ { name: '', - label: 'All', + label: 'intl.all', href: '/notifications' }, { name: 'mentions', - label: 'Mentions', + label: 'intl.mentions', href: '/notifications/mentions' } ] diff --git a/src/routes/_components/ShortcutHelpInfo.html b/src/routes/_components/ShortcutHelpInfo.html index 47532505..3e12dc7a 100644 --- a/src/routes/_components/ShortcutHelpInfo.html +++ b/src/routes/_components/ShortcutHelpInfo.html @@ -1,61 +1,26 @@ <div class="shortcut-help-info {inDialog ? 'in-dialog' : ''}" tabindex="{inDialog ? '0' : '-1'}" > - <!-- Svelte makes this file kind of ridiculously large for a static page (~17kB), - so just use raw HTML here to make it smaller --> -{@html ` - <h2>Global</h2> + <h2>{intl.global}</h2> <div class="hotkey-group"> - ${$leftRightChangesFocus ? - ` + <ul> + {@html globalHotkeysText} + </ul> + </div> + <h2>{intl.timeline}</h2> + <div class="hotkey-group"> + <ul> + {@html intl.timelineHotkeys} + </ul> + </div> + {#if !$leftRightChangesFocus} + <h2>{intl.media}</h2> + <div class="hotkey-group"> <ul> - <li><kbd>→</kbd> to go to the next focusable element</li> - <li><kbd>←</kbd> to go to the previous focusable element</li> + {@html intl.mediaHotkeys} </ul> - ` : ''} - <ul> - <li> - <kbd>1</kbd> - <kbd>6</kbd> - ${$leftRightChangesFocus ? '' : `or <kbd>←</kbd>/<kbd>→</kbd>`} - to switch columns - </li> - <li><kbd>7</kbd> or <kbd>c</kbd> to compose a new toot</li> - <li><kbd>s</kbd> or <kbd>/</kbd> to search</li> - <li><kbd>g</kbd> + <kbd>h</kbd> to go home</li> - <li><kbd>g</kbd> + <kbd>n</kbd> to go to notifications</li> - <li><kbd>g</kbd> + <kbd>l</kbd> to go to the local timeline</li> - <li><kbd>g</kbd> + <kbd>t</kbd> to go to the federated timeline</li> - <li><kbd>g</kbd> + <kbd>c</kbd> to go to the community page</li> - <li><kbd>g</kbd> + <kbd>d</kbd> to go to the conversations page</li> - <li><kbd>h</kbd> or <kbd>?</kbd> to toggle the help dialog</li> - <li><kbd>Backspace</kbd> to go back, close dialogs</li> - </ul> - </div> - <h2>Timeline</h2> - <div class="hotkey-group"> - <ul> - <li><kbd>j</kbd> or <kbd>↓</kbd> to activate the next toot</li> - <li><kbd>k</kbd> or <kbd>↑</kbd> to activate the previous toot</li> - <li><kbd>.</kbd> to show more and scroll to top</li> - <li><kbd>o</kbd> to open</li> - <li><kbd>f</kbd> to favorite</li> - <li><kbd>b</kbd> to boost</li> - <li><kbd>r</kbd> to reply</li> - <li><kbd>i</kbd> to open images, video, or audio</li> - <li><kbd>y</kbd> to show or hide sensitive media</li> - <li><kbd>m</kbd> to mention the author</li> - <li><kbd>p</kbd> to open the author's profile</li> - <li><kbd>l</kbd> to open the card's link in a new tab</li> - <li><kbd>x</kbd> to show or hide text behind content warning</li> - </ul> - </div> - <h2>Media</h2> - <div class="hotkey-group"> - <ul> - <li><kbd>←</kbd> / <kbd>→</kbd> to go to next or previous</li> - </ul> - </div> -`} + </div> + {/if} </div> <style> .shortcut-help-info.in-dialog { @@ -96,11 +61,17 @@ </style> <script> import { store } from '../_store/store' + import { formatIntl } from '../_utils/formatIntl' export default { store: () => store, data: () => ({ inDialog: false - }) + }), + computed: { + globalHotkeysText: ({ $leftRightChangesFocus }) => ( + formatIntl('intl.globalHotkeys', { leftRightChangesFocus: $leftRightChangesFocus }) + ) + } } </script> diff --git a/src/routes/_components/SvgIconLegacy.html b/src/routes/_components/SvgIconLegacy.html deleted file mode 100644 index c086edc0..00000000 --- a/src/routes/_components/SvgIconLegacy.html +++ /dev/null @@ -1,36 +0,0 @@ -<!-- old browsers can't handle <use> very well --> -<svg - class={className} - {style} - aria-hidden={!ariaLabel} - aria-label={ariaLabel} - {viewBox} - ref:svg> - {@html html} -</svg> -<script> - import { animate } from '../_utils/animate' - import { store } from '../_store/store' - - export default { - data: () => ({ - className: '', - style: '', - ariaLabel: '' - }), - store: () => store, - computed: { - svgData: ({ href }) => process.env.ALL_SVGS[href], - html: ({ svgData }) => svgData.html, - viewBox: ({ svgData }) => svgData.viewBox - }, - methods: { - animate (animation) { - const { reduceMotion } = this.store.get() - if (animation && !reduceMotion) { - animate(this.refs.svg, animation) - } - } - } - } -</script> diff --git a/src/routes/_components/TabSet.html b/src/routes/_components/TabSet.html index 10d29a74..d266f190 100644 --- a/src/routes/_components/TabSet.html +++ b/src/routes/_components/TabSet.html @@ -2,7 +2,8 @@ <ul> {#each tabs as tab (tab.name)} <li class="{currentTabName === tab.name ? 'current' : 'not-current'}"> - <a aria-label="{tab.label} { currentTabName === tab.name ? '(Current)' : ''}" + <a aria-label={createAriaLabel(tab.label, tab.name, currentTabName)} + aria-current={tab.name === currentTabName} class="focus-fix" href={tab.href} rel="prefetch"> @@ -83,9 +84,19 @@ } </style> <script> + import { formatIntl } from '../_utils/formatIntl' + export default { data: () => ({ className: '' - }) + }), + helpers: { + createAriaLabel (tabLabel, tabName, currentTabName) { + return formatIntl('intl.tabLabel', { + label: tabLabel, + current: tabName === currentTabName + }) + } + } } </script> diff --git a/src/routes/_components/Title.html b/src/routes/_components/Title.html index bd1482c8..1c9db72b 100644 --- a/src/routes/_components/Title.html +++ b/src/routes/_components/Title.html @@ -1,8 +1,9 @@ <svelte:head> - <title>{notificationsIndicator}{instanceIndicator} · {name} + {title} diff --git a/src/routes/_components/community/PageListItem.html b/src/routes/_components/community/PageListItem.html index df34ed0c..34f93839 100644 --- a/src/routes/_components/community/PageListItem.html +++ b/src/routes/_components/community/PageListItem.html @@ -9,7 +9,7 @@ id="pinnables" className="pinnable-button" checked={$pinnedPage === href} - label="Pin {label}" + label={pinLabel} index={pinIndex} on:click="onPinClick(event)" > @@ -119,6 +119,7 @@ import { store } from '../../_store/store' import SvgIcon from '../SvgIcon.html' import RadioGroupButton from '../../_components/radio/RadioGroupButton.html' + import { formatIntl } from '../../_utils/formatIntl' export default { store: () => store, @@ -128,12 +129,13 @@ }), computed: { ariaLabel: ({ label, pinnable, $pinnedPage, href }) => { - let res = label - if (pinnable) { - res += ' (' + ($pinnedPage === href ? 'Pinned page' : 'Unpinned page') + ')' - } - return res - } + return formatIntl('intl.pinLabel', { + label, + pinnable, + pinned: $pinnedPage === href + }) + }, + pinLabel: ({ label }) => formatIntl('intl.pinPage', { label }) }, components: { SvgIcon, diff --git a/src/routes/_components/compose/ComposeBox.html b/src/routes/_components/compose/ComposeBox.html index dc16cd52..a27c8c05 100644 --- a/src/routes/_components/compose/ComposeBox.html +++ b/src/routes/_components/compose/ComposeBox.html @@ -1,5 +1,5 @@ {#if realm === 'home'} -

Compose status

+

{intl.composeStatus}

{/if}
diff --git a/src/routes/_components/compose/ComposeButton.html b/src/routes/_components/compose/ComposeButton.html index f5de129e..8478b0e3 100644 --- a/src/routes/_components/compose/ComposeButton.html +++ b/src/routes/_components/compose/ComposeButton.html @@ -1,10 +1,10 @@