Kaynağa Gözat

Fix translations that did not respect word order (#2077)

* Fix translations that did not respect word order

Fixes #2069

We were still using raw string concatenation in these two places. To
maintain backwards compatibility, this commit adds _new_ translations
that are substitution-based instead of concatenation-based. The old
translation is passed in as a possible substitution parameter
`backwardsCompat`, and the default (English) translations use this
parameter.

With this, if you are using a custom locale that overrides eg. the
`exceedsSize` string, Uppy will first get your `exceedsSize`
translation, and then use the default `exceedsSize2` translation which
concatenates that result and the file size. So it works the same as it
did in the past.
If a translation requires a different word order, it can override the
`exceedsSize2` translation instead.
```js
new Uppy({
  locale: {
    strings: {
      exceedsSize2: 'Maximum file size of %{size} exceeded',
      poweredBy2: '%{uppy} made this!'
    }
  }
})
```

In 2.0, we can remove all the old strings and only use `poweredBy2` (and
probably rename them to `poweredBy` too, heh)

* website: add missing dashboard strings to docs

* locales: remove unused second plural form from EN and NL

* build: allow different numbers of plural forms

* utils: add test for new throwing behaviour

* docs: insert PR url

* locales: revert ko_KR changes

* changelog: add 2.0 backlog entry

* locales: var → const

* docs: add brief note that translators should supply a pluralize function

* locales: update en_US with new template

* fix HEIGHT_MD = 400

Co-authored-by: Artur Paikin <artur@arturpaikin.com>
Renée Kooi 5 yıl önce
ebeveyn
işleme
5e25b2a939

+ 1 - 0
CHANGELOG.md

@@ -49,6 +49,7 @@ PRs are welcome! Please do open an issue to discuss first if it's a big feature,
 - [ ] core: maybe we remove `file.name` and only keep `file.meta.name`; we can change the file.name here actually because it's just a plain object. we can't change the file.data.name where data is a File instance from an input or something. For XHRUpload, where we put the File instance in a FormData object and it uses the unchangeable .name property.
 - [ ] core: pass full file object to `onBeforeFileAdded`. Maybe also check restrictions before calling the callbacks: https://github.com/transloadit/uppy/pull/1594
 - [ ] core: remove `debug`, we have `logger` and `logger: Uppy.debugLogger` for that now
+- [ ] core/dashboard: replace `poweredBy` and `exceedsSize` locale keys by word order aware versions, see PR #2077
 - [ ] *: upgrade to Preact X
 - [ ] dashboard: hiding pause/resume from the UI by default (with option) would be good too probably (we could auto pause and show a resume button when detecting a network change to a metered network using https://devdocs.io/dom/networkinformation/type)
 - [ ] dashboard: showing links to files should be turned off by default (it's great for devs, they can opt-in, but for end-user UI it's weird and can even lead to problems though)

+ 3 - 3
bin/locale-packs.js

@@ -2,7 +2,6 @@ const glob = require('glob')
 const Uppy = require('../packages/@uppy/core')
 const chalk = require('chalk')
 const path = require('path')
-const flat = require('flat')
 const stringifyObject = require('stringify-object')
 const fs = require('fs')
 const uppy = Uppy()
@@ -219,8 +218,9 @@ function test () {
     // for backwards-compat, see https://github.com/transloadit/uppy/pull/1929
     if (localeName === 'es_GL') return
 
-    // Builds array with items like: 'uploadingXFiles.2'
-    followerValues[localeName] = flat(require(localePath).strings)
+    // Builds array with items like: 'uploadingXFiles'
+    // We do not check nested items because different languages may have different amounts of plural forms.
+    followerValues[localeName] = Object.keys(require(localePath).strings)
     followerLocales[localeName] = Object.keys(followerValues[localeName])
   })
 

+ 0 - 1
package.json

@@ -138,7 +138,6 @@
     "exorcist": "1.0.1",
     "express": "4.17.1",
     "fakefile": "0.0.9",
-    "flat": "4.1.0",
     "github-contributors-list": "1.2.4",
     "glob": "7.1.6",
     "globby": "9.2.0",

+ 14 - 9
packages/@uppy/core/src/index.js

@@ -41,14 +41,18 @@ class Uppy {
         },
         youCanOnlyUploadX: {
           0: 'You can only upload %{smart_count} file',
-          1: 'You can only upload %{smart_count} files',
-          2: 'You can only upload %{smart_count} files'
+          1: 'You can only upload %{smart_count} files'
         },
         youHaveToAtLeastSelectX: {
           0: 'You have to select at least %{smart_count} file',
-          1: 'You have to select at least %{smart_count} files',
-          2: 'You have to select at least %{smart_count} files'
+          1: 'You have to select at least %{smart_count} files'
         },
+        // The default `exceedsSize2` string only combines the `exceedsSize` string (%{backwardsCompat}) with the size.
+        // Locales can override `exceedsSize2` to specify a different word order. This is for backwards compat with
+        // Uppy 1.9.x and below which did a naive concatenation of `exceedsSize2 + size` instead of using a locale-specific
+        // substitution.
+        // TODO: In 2.0 `exceedsSize2` should be removed in and `exceedsSize` updated to use substitution.
+        exceedsSize2: '%{backwardsCompat} %{size}',
         exceedsSize: 'This file exceeds maximum allowed size of',
         youCanOnlyUploadFileTypes: 'You can only upload: %{types}',
         noNewAlreadyUploading: 'Cannot add new files: already uploading',
@@ -63,8 +67,7 @@ class Uppy {
         noFilesFound: 'You have no files or folders here',
         selectX: {
           0: 'Select %{smart_count}',
-          1: 'Select %{smart_count}',
-          2: 'Select %{smart_count}'
+          1: 'Select %{smart_count}'
         },
         selectAllFilesFromFolderNamed: 'Select all files from folder %{name}',
         unselectAllFilesFromFolderNamed: 'Unselect all files from folder %{name}',
@@ -81,8 +84,7 @@ class Uppy {
         emptyFolderAdded: 'No files were added from empty folder',
         folderAdded: {
           0: 'Added %{smart_count} file from %{folder}',
-          1: 'Added %{smart_count} files from %{folder}',
-          2: 'Added %{smart_count} files from %{folder}'
+          1: 'Added %{smart_count} files from %{folder}'
         }
       }
     }
@@ -459,7 +461,10 @@ class Uppy {
     // We can't check maxFileSize if the size is unknown.
     if (maxFileSize && file.data.size != null) {
       if (file.data.size > maxFileSize) {
-        throw new RestrictionError(`${this.i18n('exceedsSize')} ${prettyBytes(maxFileSize)}`)
+        throw new RestrictionError(this.i18n('exceedsSize2', {
+          backwardsCompat: this.i18n('exceedsSize'),
+          size: prettyBytes(maxFileSize)
+        }))
       }
     }
   }

+ 16 - 5
packages/@uppy/dashboard/src/components/AddFiles.js

@@ -32,6 +32,21 @@ class AddFiles extends Component {
   }
 
   renderPoweredByUppy () {
+    const uppyBranding = (
+      <span>
+        <svg aria-hidden="true" focusable="false" class="UppyIcon uppy-Dashboard-poweredByIcon" width="11" height="11" viewBox="0 0 11 11">
+          <path d="M7.365 10.5l-.01-4.045h2.612L5.5.806l-4.467 5.65h2.604l.01 4.044h3.718z" fill-rule="evenodd" />
+        </svg>
+        <span class="uppy-Dashboard-poweredByUppy">Uppy</span>
+      </span>
+    )
+
+    // Support both the old word-order-insensitive string `poweredBy` and the new word-order-sensitive string `poweredBy2`
+    const linkText = this.props.i18nArray('poweredBy2', {
+      backwardsCompat: this.props.i18n('poweredBy'),
+      uppy: uppyBranding
+    })
+
     return (
       <a
         tabindex="-1"
@@ -40,11 +55,7 @@ class AddFiles extends Component {
         target="_blank"
         class="uppy-Dashboard-poweredBy"
       >
-        {this.props.i18n('poweredBy') + ' '}
-        <svg aria-hidden="true" focusable="false" class="UppyIcon uppy-Dashboard-poweredByIcon" width="11" height="11" viewBox="0 0 11 11">
-          <path d="M7.365 10.5l-.01-4.045h2.612L5.5.806l-4.467 5.65h2.604l.01 4.044h3.718z" fill-rule="evenodd" />
-        </svg>
-        <span class="uppy-Dashboard-poweredByUppy">Uppy</span>
+        {linkText}
       </a>
     )
   }

+ 1 - 1
packages/@uppy/dashboard/src/components/Dashboard.js

@@ -27,7 +27,7 @@ function TransitionWrapper (props) {
 const WIDTH_XL = 900
 const WIDTH_LG = 700
 const WIDTH_MD = 576
-const HEIGHT_MD = 576
+const HEIGHT_MD = 400
 
 module.exports = function Dashboard (props) {
   const noFiles = props.totalFileCount === 0

+ 9 - 6
packages/@uppy/dashboard/src/index.js

@@ -75,19 +75,22 @@ module.exports = class Dashboard extends Plugin {
         cancelUpload: 'Cancel upload',
         xFilesSelected: {
           0: '%{smart_count} file selected',
-          1: '%{smart_count} files selected',
-          2: '%{smart_count} files selected'
+          1: '%{smart_count} files selected'
         },
         uploadingXFiles: {
           0: 'Uploading %{smart_count} file',
-          1: 'Uploading %{smart_count} files',
-          2: 'Uploading %{smart_count} files'
+          1: 'Uploading %{smart_count} files'
         },
         processingXFiles: {
           0: 'Processing %{smart_count} file',
-          1: 'Processing %{smart_count} files',
-          2: 'Processing %{smart_count} files'
+          1: 'Processing %{smart_count} files'
         },
+        // The default `poweredBy2` string only combines the `poweredBy` string (%{backwardsCompat}) with the size.
+        // Locales can override `poweredBy2` to specify a different word order. This is for backwards compat with
+        // Uppy 1.9.x and below which did a naive concatenation of `poweredBy2 + size` instead of using a locale-specific
+        // substitution.
+        // TODO: In 2.0 `poweredBy2` should be removed in and `poweredBy` updated to use substitution.
+        poweredBy2: '%{backwardsCompat} %{uppy}',
         poweredBy: 'Powered by'
       }
     }

+ 1 - 1
packages/@uppy/dashboard/src/style.scss

@@ -251,7 +251,7 @@
   flex-grow: 0;
   flex-shrink: 1;
   flex-basis: 0%;
-  
+
   // hide on short note and “powered by” on short screens
   // such as CodePen, or inline dashboard with height < 400px
   display: none;

+ 15 - 24
packages/@uppy/locales/src/en_US.js

@@ -44,20 +44,19 @@ en_US.strings = {
   enterCorrectUrl: 'Incorrect URL: Please make sure you are entering a direct link to a file',
   enterUrlToImport: 'Enter URL to import a file',
   exceedsSize: 'This file exceeds maximum allowed size of',
+  exceedsSize2: '%{backwardsCompat} %{size}',
   failedToFetch: 'Companion failed to fetch this URL, please make sure it’s correct',
   failedToUpload: 'Failed to upload %{file}',
   fileSource: 'File source: %{name}',
   filesUploadedOfTotal: {
     '0': '%{complete} of %{smart_count} file uploaded',
-    '1': '%{complete} of %{smart_count} files uploaded',
-    '2': '%{complete} of %{smart_count} files uploaded'
+    '1': '%{complete} of %{smart_count} files uploaded'
   },
   filter: 'Filter',
   finishEditingFile: 'Finish editing file',
   folderAdded: {
     '0': 'Added %{smart_count} file from %{folder}',
-    '1': 'Added %{smart_count} files from %{folder}',
-    '2': 'Added %{smart_count} files from %{folder}'
+    '1': 'Added %{smart_count} files from %{folder}'
   },
   generatingThumbnails: 'Generating thumbnails...',
   import: 'Import',
@@ -75,11 +74,11 @@ en_US.strings = {
   pauseUpload: 'Pause upload',
   paused: 'Paused',
   poweredBy: 'Powered by',
+  poweredBy2: '%{backwardsCompat} %{uppy}',
   preparingUpload: 'Preparing upload...',
   processingXFiles: {
     '0': 'Processing %{smart_count} file',
-    '1': 'Processing %{smart_count} files',
-    '2': 'Processing %{smart_count} files'
+    '1': 'Processing %{smart_count} files'
   },
   recordingLength: 'Recording length %{recording_length}',
   removeFile: 'Remove file',
@@ -93,8 +92,7 @@ en_US.strings = {
   selectFileNamed: 'Select file %{name}',
   selectX: {
     '0': 'Select %{smart_count}',
-    '1': 'Select %{smart_count}',
-    '2': 'Select %{smart_count}'
+    '1': 'Select %{smart_count}'
   },
   smile: 'Smile!',
   startRecording: 'Begin video recording',
@@ -109,46 +107,39 @@ en_US.strings = {
   uploadPaused: 'Upload paused',
   uploadXFiles: {
     '0': 'Upload %{smart_count} file',
-    '1': 'Upload %{smart_count} files',
-    '2': 'Upload %{smart_count} files'
+    '1': 'Upload %{smart_count} files'
   },
   uploadXNewFiles: {
     '0': 'Upload +%{smart_count} file',
-    '1': 'Upload +%{smart_count} files',
-    '2': 'Upload +%{smart_count} files'
+    '1': 'Upload +%{smart_count} files'
   },
   uploading: 'Uploading',
   uploadingXFiles: {
     '0': 'Uploading %{smart_count} file',
-    '1': 'Uploading %{smart_count} files',
-    '2': 'Uploading %{smart_count} files'
+    '1': 'Uploading %{smart_count} files'
   },
   xFilesSelected: {
     '0': '%{smart_count} file selected',
-    '1': '%{smart_count} files selected',
-    '2': '%{smart_count} files selected'
+    '1': '%{smart_count} files selected'
   },
   xMoreFilesAdded: {
     '0': '%{smart_count} more file added',
-    '1': '%{smart_count} more files added',
-    '2': '%{smart_count} more files added'
+    '1': '%{smart_count} more files added'
   },
   xTimeLeft: '%{time} left',
   youCanOnlyUploadFileTypes: 'You can only upload: %{types}',
   youCanOnlyUploadX: {
     '0': 'You can only upload %{smart_count} file',
-    '1': 'You can only upload %{smart_count} files',
-    '2': 'You can only upload %{smart_count} files'
+    '1': 'You can only upload %{smart_count} files'
   },
   youHaveToAtLeastSelectX: {
     '0': 'You have to select at least %{smart_count} file',
-    '1': 'You have to select at least %{smart_count} files',
-    '2': 'You have to select at least %{smart_count} files'
+    '1': 'You have to select at least %{smart_count} files'
   }
 }
 
-en_US.pluralize = function (n) {
-  if (n === 1) {
+en_US.pluralize = function (count) {
+  if (count === 1) {
     return 0
   }
   return 1

+ 2 - 1
packages/@uppy/locales/src/ko_KR.js

@@ -1,4 +1,5 @@
-var ko_KR = {}
+const ko_KR = {}
+
 ko_KR.strings = {
   addMore: '파일 추가',
   addMoreFiles: '파일 추가',

+ 13 - 22
packages/@uppy/locales/src/nl_NL.js

@@ -38,21 +38,20 @@ nl_NL.strings = {
   encoding: 'Coderen...',
   enterCorrectUrl: 'Ongeldige URL: Zorg dat je een directe link naar een bestand invoert',
   enterUrlToImport: 'Voeg URL toe om een bestand te importeren',
+  exceedsSize2: 'Dit bestand overschrijdt de maximaal toegelaten bestandsgrootte van %{size}',
   exceedsSize: 'Dit bestand overschrijdt de maximaal toegelaten bestandsgrootte van',
   failedToFetch: 'Companion kan deze URL niet laden, controleer of de URL correct is',
   failedToUpload: 'Kon %{file} niet uploaden',
   fileSource: 'Bronbestand: %{name}',
   filesUploadedOfTotal: {
     '0': '%{complete} van %{smart_count} bestand geüpload',
-    '1': '%{complete} van %{smart_count} bestanden geüpload',
-    '2': '%{complete} van %{smart_count} bestanden geüpload'
+    '1': '%{complete} van %{smart_count} bestanden geüpload'
   },
   filter: 'Filter',
   finishEditingFile: 'Klaar met bestand aan te passen',
   folderAdded: {
     '0': '%{smart_count} bestand uit %{folder} toegevoegd',
-    '1': '%{smart_count} bestanden uit %{folder} toegevoegd',
-    '2': '%{smart_count} bestanden uit %{folder} toegevoegd'
+    '1': '%{smart_count} bestanden uit %{folder} toegevoegd'
   },
   import: 'Importeer',
   importFrom: 'Importeer vanuit %{name}',
@@ -65,12 +64,12 @@ nl_NL.strings = {
   pause: 'Pauze',
   pauseUpload: 'Pauzeer upload',
   paused: 'Gepauzeerd',
+  poweredBy2: 'Mogelijk gemaakt door %{uppy}',
   poweredBy: 'Mogelijk gemaakt door',
   preparingUpload: 'Upload voorbereiden...',
   processingXFiles: {
     '0': 'Bezig met %{smart_count} bestand te verwerken',
-    '1': 'Bezig met %{smart_count} bestanden te verwerken',
-    '2': 'Bezig met %{smart_count} bestanden te verwerken'
+    '1': 'Bezig met %{smart_count} bestanden te verwerken'
   },
   recordingLength: 'Opnameduur %{recording_length}',
   removeFile: 'Bestand verwijderen',
@@ -82,8 +81,7 @@ nl_NL.strings = {
   saveChanges: 'Wijzigingen opslaan',
   selectX: {
     '0': 'Selecteer %{smart_count}',
-    '1': 'Selecteer %{smart_count}',
-    '2': 'Selecteer %{smart_count}'
+    '1': 'Selecteer %{smart_count}'
   },
   smile: 'Lach!',
   startRecording: 'Start video-opname',
@@ -96,41 +94,34 @@ nl_NL.strings = {
   uploadPaused: 'Upload gepauzeerd',
   uploadXFiles: {
     '0': 'Upload %{smart_count} bestand',
-    '1': 'Upload %{smart_count} bestanden',
-    '2': 'Upload %{smart_count} bestanden'
+    '1': 'Upload %{smart_count} bestanden'
   },
   uploadXNewFiles: {
     '0': 'Upload +%{smart_count} bestand',
-    '1': 'Upload +%{smart_count} bestanden',
-    '2': 'Upload +%{smart_count} bestanden'
+    '1': 'Upload +%{smart_count} bestanden'
   },
   uploading: 'Bezig met uploaden',
   uploadingXFiles: {
     '0': 'Bezig met %{smart_count} bestand te uploaden',
-    '1': 'Bezig met %{smart_count} bestanden te uploaden',
-    '2': 'Bezig met %{smart_count} bestanden te uploaden'
+    '1': 'Bezig met %{smart_count} bestanden te uploaden'
   },
   xFilesSelected: {
     '0': '%{smart_count} bestand geselecteerd',
-    '1': '%{smart_count} bestanden geselecteerd',
-    '2': '%{smart_count} bestanden geselecteerd'
+    '1': '%{smart_count} bestanden geselecteerd'
   },
   xMoreFilesAdded: {
     '0': '%{smart_count} extra bestand toegevoegd',
-    '1': '%{smart_count} extra bestanden toegevoegd',
-    '2': '%{smart_count} extra bestanden toegevoegd'
+    '1': '%{smart_count} extra bestanden toegevoegd'
   },
   xTimeLeft: '%{time} over',
   youCanOnlyUploadFileTypes: 'Je kan enkel volgende types uploaden: %{types}',
   youCanOnlyUploadX: {
     '0': 'Je kan slechts %{smart_count} bestand uploaden',
-    '1': 'Je kan slechts %{smart_count} bestanden uploaden',
-    '2': 'Je kan slechts %{smart_count} bestanden uploaden'
+    '1': 'Je kan slechts %{smart_count} bestanden uploaden'
   },
   youHaveToAtLeastSelectX: {
     '0': 'Je moet minstens %{smart_count} bestand selecteren',
-    '1': 'Je moet minstens %{smart_count} bestanden selecteren',
-    '2': 'Je moet minstens %{smart_count} bestanden selecteren'
+    '1': 'Je moet minstens %{smart_count} bestanden selecteren'
   },
   selectAllFilesFromFolderNamed: 'Selecteer alle bestanden uit de map %{name}',
   unselectAllFilesFromFolderNamed: 'Deselecteer alle bestanden uit de map %{name}',

+ 2 - 2
packages/@uppy/locales/template.js

@@ -2,8 +2,8 @@ const en_US = {}
 
 en_US.strings = {}
 
-en_US.pluralize = function (n) {
-  if (n === 1) {
+en_US.pluralize = function (count) {
+  if (count === 1) {
     return 0
   }
   return 1

+ 4 - 8
packages/@uppy/status-bar/src/index.js

@@ -31,25 +31,21 @@ module.exports = class StatusBar extends Plugin {
         resume: 'Resume',
         filesUploadedOfTotal: {
           0: '%{complete} of %{smart_count} file uploaded',
-          1: '%{complete} of %{smart_count} files uploaded',
-          2: '%{complete} of %{smart_count} files uploaded'
+          1: '%{complete} of %{smart_count} files uploaded'
         },
         dataUploadedOfTotal: '%{complete} of %{total}',
         xTimeLeft: '%{time} left',
         uploadXFiles: {
           0: 'Upload %{smart_count} file',
-          1: 'Upload %{smart_count} files',
-          2: 'Upload %{smart_count} files'
+          1: 'Upload %{smart_count} files'
         },
         uploadXNewFiles: {
           0: 'Upload +%{smart_count} file',
-          1: 'Upload +%{smart_count} files',
-          2: 'Upload +%{smart_count} files'
+          1: 'Upload +%{smart_count} files'
         },
         xMoreFilesAdded: {
           0: '%{smart_count} more file added',
-          1: '%{smart_count} more files added',
-          2: '%{smart_count} more files added'
+          1: '%{smart_count} more files added'
         }
       }
     }

+ 11 - 4
packages/@uppy/utils/src/Translator.js

@@ -117,11 +117,18 @@ module.exports = class Translator {
    * @returns {Array} The translated and interpolated parts, in order.
    */
   translateArray (key, options) {
-    if (options && typeof options.smart_count !== 'undefined') {
-      var plural = this.locale.pluralize(options.smart_count)
-      return this.interpolate(this.locale.strings[key][plural], options)
+    const string = this.locale.strings[key]
+    const hasPluralForms = typeof string === 'object'
+
+    if (hasPluralForms) {
+      if (options && typeof options.smart_count !== 'undefined') {
+        const plural = this.locale.pluralize(options.smart_count)
+        return this.interpolate(string[plural], options)
+      } else {
+        throw new Error('Attempted to use a string with plural forms, but no value was given for %{smart_count}')
+      }
     }
 
-    return this.interpolate(this.locale.strings[key], options)
+    return this.interpolate(string, options)
   }
 }

+ 34 - 0
packages/@uppy/utils/src/Translator.test.js

@@ -114,5 +114,39 @@ describe('Translator', () => {
         translator.translate('filesChosen', { smart_count: 0 })
       ).toEqual('Выбрано 0 файлов')
     })
+
+    it('should support strings without plural forms', () => {
+      const translator = new Translator({
+        strings: {
+          theAmount: 'het aantal is %{smart_count}'
+        },
+        pluralize: () => 0
+      })
+
+      expect(
+        translator.translate('theAmount', { smart_count: 0 })
+      ).toEqual('het aantal is 0')
+      expect(
+        translator.translate('theAmount', { smart_count: 1 })
+      ).toEqual('het aantal is 1')
+      expect(
+        translator.translate('theAmount', { smart_count: 1202530 })
+      ).toEqual('het aantal is 1202530')
+    })
+
+    it('should error when using a plural form without %{smart_count}', () => {
+      const translator = new Translator({
+        strings: {
+          test: {
+            0: 'A test',
+            1: '%{smart_count} tests'
+          }
+        }
+      })
+
+      expect(() => {
+        translator.translate('test')
+      }).toThrow('Attempted to use a string with plural forms, but no value was given for %{smart_count}')
+    })
   })
 })

+ 27 - 0
website/src/docs/dashboard.md

@@ -266,6 +266,8 @@ strings: {
   closeModal: 'Close Modal',
   // Used as the screen reader label for the plus (+) button that shows the “Add more files” screen
   addMoreFiles: 'Add more files',
+  // TODO
+  addingMoreFiles: 'Adding more files',
   // Used as the header for import panels, e.g., “Import from Google Drive”.
   importFrom: 'Import from %{name}',
   // When `inline: false`, used as the screen reader label for the dashboard modal.
@@ -283,6 +285,8 @@ strings: {
   fileSource: 'File source: %{name}',
   // Used as the label for buttons that accept and close panels (remote providers or metadata editor)
   done: 'Done',
+  // TODO
+  back: 'Back',
   // Used as the screen reader label for buttons that remove a file.
   removeFile: 'Remove file',
   // Used as the screen reader label for buttons that open the metadata editor panel for a file.
@@ -294,6 +298,8 @@ strings: {
   // Used as the screen reader label for the button that saves metadata edits and returns to the
   // file list view.
   finishEditingFile: 'Finish editing file',
+  // TODO
+  saveChanges: 'Save changes',
   // Used as the label for the tab button that opens the system file selection dialog.
   myDevice: 'My Device',
   // Shown in the main dashboard area when no files have been selected, and one or more
@@ -304,23 +310,44 @@ strings: {
   // plugins are in use. %{browse} is replaced with a link that opens the system
   // file selection dialog.
   dropPaste: 'Drop files here, paste or %{browse}',
+  // TODO
+  dropHint: 'Drop your files here',
   // This string is clickable and opens the system file selection dialog.
   browse: 'browse',
   // Used as the hover text and screen reader label for file progress indicators when
   // they have been fully uploaded.
   uploadComplete: 'Upload complete',
+  // TODO
+  uploadPaused: 'Upload paused',
   // Used as the hover text and screen reader label for the buttons to resume paused uploads.
   resumeUpload: 'Resume upload',
   // Used as the hover text and screen reader label for the buttons to pause uploads.
   pauseUpload: 'Pause upload',
   // Used as the hover text and screen reader label for the buttons to retry failed uploads.
   retryUpload: 'Retry upload',
+  // Used as the hover text and screen reader label for the buttons to cancel uploads.
+  cancelUpload: 'Cancel upload',
 
   // Used in a title, how many files are currently selected
   xFilesSelected: {
     0: '%{smart_count} file selected',
     1: '%{smart_count} files selected'
   },
+  // TODO
+  uploadingXFiles: {
+    0: 'Uploading %{smart_count} file',
+    1: 'Uploading %{smart_count} files'
+  },
+  // TODO
+  processingXFiles: {
+    0: 'Processing %{smart_count} file',
+    1: 'Processing %{smart_count} files'
+  },
+
+  // The "powered by Uppy" link at the bottom of the Dashboard.
+  // **NOTE**: This string is called `poweredBy2` for backwards compatibility reasons.
+  // See https://github.com/transloadit/uppy/pull/2077
+  poweredBy2: 'Powered by %{uppy}',
 
   // @uppy/status-bar strings:
   uploading: 'Uploading',

+ 5 - 5
website/src/docs/locales.md

@@ -67,7 +67,6 @@ uppy.use(DragDrop, {
     }
   }
 })
-
 ```
 
 ## List of locale packs
@@ -79,7 +78,8 @@ uppy.use(DragDrop, {
 If you speak a language we don’t yet support, you can contribute! Here’s how you do it:
 
 1. Go to the [uppy/locales](https://github.com/transloadit/uppy/tree/master/packages/%40uppy/locales/src) directory in the Uppy GitHub repo.
-2. Go to `en_US.js` and copy its contents, as English is the most up-to-date locale.
-3. Press “Create new file”, name it according to the [`language_COUNTRY` format](http://www.i18nguy.com/unicode/language-identifiers.html), make sure to use underscore `_` as a divider. Examples: `en_US`, `en_GB`, `ru_RU`, `ar_AE`. Variants should be trailing, e.g.: `sr_RS_Latin` for Serbian Latin vs Cyrillic.
-4. Paste what you’ve copied from `en_US.js` and use it as a starting point to translate strings into your language.
-5. When you are ready, save the file — this should create a PR that we’ll then review 🎉 Thanks!
+1. Go to `en_US.js` and copy its contents, as English is the most up-to-date locale.
+1. Press “Create new file”, name it according to the [`language_COUNTRY` format](http://www.i18nguy.com/unicode/language-identifiers.html), make sure to use underscore `_` as a divider. Examples: `en_US`, `en_GB`, `ru_RU`, `ar_AE`. Variants should be trailing, e.g.: `sr_RS_Latin` for Serbian Latin vs Cyrillic.
+1. If your language has different pluralization rules than English, update the `pluralize` implementation. If you are unsure how to do this, please ask us for help in a [GitHub issue](https://github.com/transloadit/uppy/issues/new).
+1. Paste what you’ve copied from `en_US.js` and use it as a starting point to translate strings into your language.
+1. When you are ready, save the file — this should create a PR that we’ll then review 🎉 Thanks!

+ 3 - 1
website/src/docs/uppy.md

@@ -295,7 +295,9 @@ locale: {
       0: 'You have to select at least %{smart_count} file',
       1: 'You have to select at least %{smart_count} files'
     },
-    exceedsSize: 'This file exceeds maximum allowed size of',
+    // **NOTE**: This string is called `exceedsSize2` for backwards compatibility reasons.
+    // See https://github.com/transloadit/uppy/pull/2077
+    exceedsSize2: 'This file exceeds maximum allowed size of %{size}',
     youCanOnlyUploadFileTypes: 'You can only upload: %{types}',
     companionError: 'Connection with Companion failed'
   }