diff --git a/packages/@uppy/core/types/index.d.ts b/packages/@uppy/core/types/index.d.ts
index e9f323283..7c0b39ea3 100644
--- a/packages/@uppy/core/types/index.d.ts
+++ b/packages/@uppy/core/types/index.d.ts
@@ -1,3 +1,6 @@
+// This references the old types on purpose, to make sure to not create breaking changes for TS consumers.
+// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+///
import * as UppyUtils from '@uppy/utils'
// Utility types
diff --git a/packages/@uppy/utils/package.json b/packages/@uppy/utils/package.json
index 4b0fb4416..6e8c64085 100644
--- a/packages/@uppy/utils/package.json
+++ b/packages/@uppy/utils/package.json
@@ -72,6 +72,7 @@
"preact": "^10.5.13"
},
"devDependencies": {
+ "@types/lodash": "^4.14.199",
"vitest": "^0.34.5"
}
}
diff --git a/packages/@uppy/utils/src/AbortController.js b/packages/@uppy/utils/src/AbortController.js
deleted file mode 100644
index 366cb2c82..000000000
--- a/packages/@uppy/utils/src/AbortController.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import hasOwnProperty from './hasProperty.js'
-/**
- * Little AbortController proxy module so we can swap out the implementation easily later.
- */
-export const { AbortController } = globalThis
-export const { AbortSignal } = globalThis
-export const createAbortError = (message = 'Aborted', options) => {
- const err = new DOMException(message, 'AbortError')
- if (options != null && hasOwnProperty(options, 'cause')) {
- Object.defineProperty(err, 'cause', { __proto__: null, configurable: true, writable: true, value: options.cause })
- }
- return err
-}
diff --git a/packages/@uppy/utils/src/AbortController.test.js b/packages/@uppy/utils/src/AbortController.test.ts
similarity index 93%
rename from packages/@uppy/utils/src/AbortController.test.js
rename to packages/@uppy/utils/src/AbortController.test.ts
index b5f323532..f807ee27b 100644
--- a/packages/@uppy/utils/src/AbortController.test.js
+++ b/packages/@uppy/utils/src/AbortController.test.ts
@@ -1,8 +1,8 @@
import { describe, expect, it, vi } from 'vitest'
-import { AbortController, AbortSignal } from './AbortController.js'
+import { AbortController, AbortSignal } from './AbortController.ts'
-function flushInstantTimeouts () {
- return new Promise(resolve => setTimeout(resolve, 0))
+function flushInstantTimeouts() {
+ return new Promise((resolve) => setTimeout(resolve, 0))
}
describe('AbortController', () => {
diff --git a/packages/@uppy/utils/src/AbortController.ts b/packages/@uppy/utils/src/AbortController.ts
new file mode 100644
index 000000000..361c2bfba
--- /dev/null
+++ b/packages/@uppy/utils/src/AbortController.ts
@@ -0,0 +1,22 @@
+import hasOwnProperty from './hasProperty.ts'
+/**
+ * Little AbortController proxy module so we can swap out the implementation easily later.
+ */
+export const { AbortController } = globalThis
+export const { AbortSignal } = globalThis
+export const createAbortError = (
+ message = 'Aborted',
+ options?: Parameters[1],
+): DOMException => {
+ const err = new DOMException(message, 'AbortError')
+ if (options != null && hasOwnProperty(options, 'cause')) {
+ Object.defineProperty(err, 'cause', {
+ // @ts-expect-error TS is drunk
+ __proto__: null,
+ configurable: true,
+ writable: true,
+ value: options.cause,
+ })
+ }
+ return err
+}
diff --git a/packages/@uppy/utils/src/ErrorWithCause.js b/packages/@uppy/utils/src/ErrorWithCause.js
deleted file mode 100644
index 63a34c6f0..000000000
--- a/packages/@uppy/utils/src/ErrorWithCause.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import hasProperty from './hasProperty.js'
-
-class ErrorWithCause extends Error {
- constructor (message, options = {}) {
- super(message)
- this.cause = options.cause
- if (this.cause && hasProperty(this.cause, 'isNetworkError')) {
- this.isNetworkError = this.cause.isNetworkError
- }
- }
-}
-
-export default ErrorWithCause
diff --git a/packages/@uppy/utils/src/ErrorWithCause.test.js b/packages/@uppy/utils/src/ErrorWithCause.test.js
deleted file mode 100644
index 9176bb65f..000000000
--- a/packages/@uppy/utils/src/ErrorWithCause.test.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import ErrorWithCause from './ErrorWithCause.js'
-import NetworkError from './NetworkError.js'
-import isNetworkError from './isNetworkError.js'
-
-describe('ErrorWithCause', () => {
- it('should support a `{ cause }` option', () => {
- const cause = new Error('cause')
- expect(new ErrorWithCause('message').cause).toEqual(undefined)
- expect(new ErrorWithCause('message', {}).cause).toEqual(undefined)
- expect(new ErrorWithCause('message', { cause }).cause).toEqual(cause)
- })
- it('should propagate isNetworkError', () => {
- const regularError = new Error('cause')
- const networkError = new NetworkError('cause')
- expect(isNetworkError(new ErrorWithCause('message', { cause: networkError }).isNetworkError)).toEqual(true)
- expect(isNetworkError(new ErrorWithCause('message', { cause: regularError }).isNetworkError)).toEqual(false)
- expect(isNetworkError(new ErrorWithCause('message', {}).isNetworkError)).toEqual(false)
- expect(isNetworkError(new ErrorWithCause('message').isNetworkError)).toEqual(false)
- })
-})
diff --git a/packages/@uppy/utils/src/ErrorWithCause.test.ts b/packages/@uppy/utils/src/ErrorWithCause.test.ts
new file mode 100644
index 000000000..4a64fadf4
--- /dev/null
+++ b/packages/@uppy/utils/src/ErrorWithCause.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest'
+import ErrorWithCause from './ErrorWithCause.ts'
+import NetworkError from './NetworkError.ts'
+
+describe('ErrorWithCause', () => {
+ it('should support a `{ cause }` option', () => {
+ const cause = new Error('cause')
+ expect(new ErrorWithCause('message').cause).toEqual(undefined)
+ expect(new ErrorWithCause('message', {}).cause).toEqual(undefined)
+ expect(new ErrorWithCause('message', { cause }).cause).toEqual(cause)
+ })
+ it('should propagate isNetworkError', () => {
+ const regularError = new Error('cause')
+ const networkError = new NetworkError('cause')
+ expect(
+ new ErrorWithCause('message', { cause: networkError }).isNetworkError,
+ ).toEqual(true)
+ expect(
+ new ErrorWithCause('message', { cause: regularError }).isNetworkError,
+ ).toEqual(false)
+ expect(new ErrorWithCause('message', {}).isNetworkError).toEqual(false)
+ expect(new ErrorWithCause('message').isNetworkError).toEqual(false)
+ })
+})
diff --git a/packages/@uppy/utils/src/ErrorWithCause.ts b/packages/@uppy/utils/src/ErrorWithCause.ts
new file mode 100644
index 000000000..31a005b12
--- /dev/null
+++ b/packages/@uppy/utils/src/ErrorWithCause.ts
@@ -0,0 +1,23 @@
+import type NetworkError from './NetworkError.ts'
+import hasProperty from './hasProperty.ts'
+
+class ErrorWithCause extends Error {
+ public isNetworkError: boolean
+
+ public cause: Error['cause']
+
+ constructor(
+ message?: ConstructorParameters[0],
+ options?: ConstructorParameters[1],
+ ) {
+ super(message)
+ this.cause = options?.cause
+ if (this.cause && hasProperty(this.cause, 'isNetworkError')) {
+ this.isNetworkError = (this.cause as NetworkError).isNetworkError
+ } else {
+ this.isNetworkError = false
+ }
+ }
+}
+
+export default ErrorWithCause
diff --git a/packages/@uppy/utils/src/EventManager.js b/packages/@uppy/utils/src/EventManager.js
deleted file mode 100644
index b675443d1..000000000
--- a/packages/@uppy/utils/src/EventManager.js
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Create a wrapper around an event emitter with a `remove` method to remove
- * all events that were added using the wrapped emitter.
- */
-export default class EventManager {
- #uppy
-
- #events = []
-
- constructor (uppy) {
- this.#uppy = uppy
- }
-
- on (event, fn) {
- this.#events.push([event, fn])
- return this.#uppy.on(event, fn)
- }
-
- remove () {
- for (const [event, fn] of this.#events.splice(0)) {
- this.#uppy.off(event, fn)
- }
- }
-
- onFilePause (fileID, cb) {
- this.on('upload-pause', (targetFileID, isPaused) => {
- if (fileID === targetFileID) {
- cb(isPaused)
- }
- })
- }
-
- onFileRemove (fileID, cb) {
- this.on('file-removed', (file) => {
- if (fileID === file.id) cb(file.id)
- })
- }
-
- onPause (fileID, cb) {
- this.on('upload-pause', (targetFileID, isPaused) => {
- if (fileID === targetFileID) {
- // const isPaused = this.#uppy.pauseResume(fileID)
- cb(isPaused)
- }
- })
- }
-
- onRetry (fileID, cb) {
- this.on('upload-retry', (targetFileID) => {
- if (fileID === targetFileID) {
- cb()
- }
- })
- }
-
- onRetryAll (fileID, cb) {
- this.on('retry-all', () => {
- if (!this.#uppy.getFile(fileID)) return
- cb()
- })
- }
-
- onPauseAll (fileID, cb) {
- this.on('pause-all', () => {
- if (!this.#uppy.getFile(fileID)) return
- cb()
- })
- }
-
- onCancelAll (fileID, eventHandler) {
- this.on('cancel-all', (...args) => {
- if (!this.#uppy.getFile(fileID)) return
- eventHandler(...args)
- })
- }
-
- onResumeAll (fileID, cb) {
- this.on('resume-all', () => {
- if (!this.#uppy.getFile(fileID)) return
- cb()
- })
- }
-}
diff --git a/packages/@uppy/utils/src/EventManager.ts b/packages/@uppy/utils/src/EventManager.ts
new file mode 100644
index 000000000..df9a717d7
--- /dev/null
+++ b/packages/@uppy/utils/src/EventManager.ts
@@ -0,0 +1,115 @@
+import type {
+ Uppy,
+ UploadPauseCallback,
+ FileRemovedCallback,
+ UploadRetryCallback,
+ GenericEventCallback,
+} from '@uppy/core'
+import type { UppyFile } from './UppyFile'
+/**
+ * Create a wrapper around an event emitter with a `remove` method to remove
+ * all events that were added using the wrapped emitter.
+ */
+export default class EventManager {
+ #uppy: Uppy
+
+ #events: Array> = []
+
+ constructor(uppy: Uppy) {
+ this.#uppy = uppy
+ }
+
+ on(
+ event: Parameters[0],
+ fn: Parameters[1],
+ ): Uppy {
+ this.#events.push([event, fn])
+ return this.#uppy.on(event, fn)
+ }
+
+ remove(): void {
+ for (const [event, fn] of this.#events.splice(0)) {
+ this.#uppy.off(event, fn)
+ }
+ }
+
+ onFilePause(fileID: UppyFile['id'], cb: (isPaused: boolean) => void): void {
+ this.on(
+ 'upload-pause',
+ (
+ targetFileID: Parameters[0],
+ isPaused: Parameters[1],
+ ) => {
+ if (fileID === targetFileID) {
+ cb(isPaused)
+ }
+ },
+ )
+ }
+
+ onFileRemove(
+ fileID: UppyFile['id'],
+ cb: (isPaused: UppyFile['id']) => void,
+ ): void {
+ this.on('file-removed', (file: Parameters>[0]) => {
+ if (fileID === file.id) cb(file.id)
+ })
+ }
+
+ onPause(fileID: UppyFile['id'], cb: (isPaused: boolean) => void): void {
+ this.on(
+ 'upload-pause',
+ (
+ targetFileID: Parameters[0],
+ isPaused: Parameters[1],
+ ) => {
+ if (fileID === targetFileID) {
+ // const isPaused = this.#uppy.pauseResume(fileID)
+ cb(isPaused)
+ }
+ },
+ )
+ }
+
+ onRetry(fileID: UppyFile['id'], cb: () => void): void {
+ this.on(
+ 'upload-retry',
+ (targetFileID: Parameters[0]) => {
+ if (fileID === targetFileID) {
+ cb()
+ }
+ },
+ )
+ }
+
+ onRetryAll(fileID: UppyFile['id'], cb: () => void): void {
+ this.on('retry-all', () => {
+ if (!this.#uppy.getFile(fileID)) return
+ cb()
+ })
+ }
+
+ onPauseAll(fileID: UppyFile['id'], cb: () => void): void {
+ this.on('pause-all', () => {
+ if (!this.#uppy.getFile(fileID)) return
+ cb()
+ })
+ }
+
+ onCancelAll(
+ fileID: UppyFile['id'],
+ eventHandler: GenericEventCallback,
+ ): void {
+ this.on('cancel-all', (...args: Parameters) => {
+ if (!this.#uppy.getFile(fileID)) return
+ eventHandler(...args)
+ })
+ }
+
+ onResumeAll(fileID: UppyFile['id'], cb: () => void): void {
+ this.on('resume-all', () => {
+ if (!this.#uppy.getFile(fileID)) return
+ cb()
+ })
+ }
+}
diff --git a/packages/@uppy/utils/src/FileProgress.ts b/packages/@uppy/utils/src/FileProgress.ts
new file mode 100644
index 000000000..1e52271b2
--- /dev/null
+++ b/packages/@uppy/utils/src/FileProgress.ts
@@ -0,0 +1,18 @@
+interface FileProgressBase {
+ progress: number
+ uploadComplete: boolean
+ percentage: number
+ bytesTotal: number
+}
+
+// FileProgress is either started or not started. We want to make sure TS doesn't
+// let us mix the two cases, and for that effect, we have one type for each case:
+export type FileProgressStarted = FileProgressBase & {
+ uploadStarted: number
+ bytesUploaded: number
+}
+export type FileProgressNotStarted = FileProgressBase & {
+ uploadStarted: null
+ bytesUploaded: false
+}
+export type FileProgress = FileProgressStarted | FileProgressNotStarted
diff --git a/packages/@uppy/utils/src/NetworkError.js b/packages/@uppy/utils/src/NetworkError.js
deleted file mode 100644
index 979c293df..000000000
--- a/packages/@uppy/utils/src/NetworkError.js
+++ /dev/null
@@ -1,11 +0,0 @@
-class NetworkError extends Error {
- constructor (error, xhr = null) {
- super(`This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.`)
-
- this.cause = error
- this.isNetworkError = true
- this.request = xhr
- }
-}
-
-export default NetworkError
diff --git a/packages/@uppy/utils/src/NetworkError.ts b/packages/@uppy/utils/src/NetworkError.ts
new file mode 100644
index 000000000..c96490727
--- /dev/null
+++ b/packages/@uppy/utils/src/NetworkError.ts
@@ -0,0 +1,19 @@
+class NetworkError extends Error {
+ public cause: unknown
+
+ public isNetworkError: true
+
+ public request: null | XMLHttpRequest
+
+ constructor(error: unknown, xhr: null | XMLHttpRequest = null) {
+ super(
+ `This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.`,
+ )
+
+ this.cause = error
+ this.isNetworkError = true
+ this.request = xhr
+ }
+}
+
+export default NetworkError
diff --git a/packages/@uppy/utils/src/ProgressTimeout.js b/packages/@uppy/utils/src/ProgressTimeout.ts
similarity index 75%
rename from packages/@uppy/utils/src/ProgressTimeout.js
rename to packages/@uppy/utils/src/ProgressTimeout.ts
index 103993531..8e6617fd4 100644
--- a/packages/@uppy/utils/src/ProgressTimeout.js
+++ b/packages/@uppy/utils/src/ProgressTimeout.ts
@@ -5,20 +5,23 @@
* Call `timer.done()` when the upload has completed.
*/
class ProgressTimeout {
- #aliveTimer
+ #aliveTimer?: ReturnType
#isDone = false
- #onTimedOut
+ #onTimedOut: Parameters[0]
- #timeout
+ #timeout: number
- constructor (timeout, timeoutHandler) {
+ constructor(
+ timeout: number,
+ timeoutHandler: Parameters[0],
+ ) {
this.#timeout = timeout
this.#onTimedOut = timeoutHandler
}
- progress () {
+ progress(): void {
// Some browsers fire another progress event when the upload is
// cancelled, so we have to ignore progress after the timer was
// told to stop.
@@ -30,10 +33,10 @@ class ProgressTimeout {
}
}
- done () {
+ done(): void {
if (!this.#isDone) {
clearTimeout(this.#aliveTimer)
- this.#aliveTimer = null
+ this.#aliveTimer = undefined
this.#isDone = true
}
}
diff --git a/packages/@uppy/utils/src/RateLimitedQueue.test.js b/packages/@uppy/utils/src/RateLimitedQueue.test.js
index 32700af09..da2b000cc 100644
--- a/packages/@uppy/utils/src/RateLimitedQueue.test.js
+++ b/packages/@uppy/utils/src/RateLimitedQueue.test.js
@@ -1,11 +1,10 @@
import { describe, expect, it } from 'vitest'
import { RateLimitedQueue } from './RateLimitedQueue.js'
-
-const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
+import delay from './delay.ts'
describe('RateLimitedQueue', () => {
let pending = 0
- function fn () {
+ function fn() {
pending++
return delay(15).then(() => pending--)
}
@@ -15,9 +14,16 @@ describe('RateLimitedQueue', () => {
const fn2 = queue.wrapPromiseFunction(fn)
const result = Promise.all([
- fn2(), fn2(), fn2(), fn2(),
- fn2(), fn2(), fn2(), fn2(),
- fn2(), fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
])
expect(pending).toBe(4)
@@ -34,9 +40,16 @@ describe('RateLimitedQueue', () => {
const fn2 = queue.wrapPromiseFunction(fn)
const result = Promise.all([
- fn2(), fn2(), fn2(), fn2(),
- fn2(), fn2(), fn2(), fn2(),
- fn2(), fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
])
expect(pending).toBe(10)
@@ -48,13 +61,22 @@ describe('RateLimitedQueue', () => {
it('should accept non-promise function in wrapPromiseFunction()', () => {
const queue = new RateLimitedQueue(1)
- function syncFn () { return 1 }
+ function syncFn() {
+ return 1
+ }
const fn2 = queue.wrapPromiseFunction(syncFn)
return Promise.all([
- fn2(), fn2(), fn2(), fn2(),
- fn2(), fn2(), fn2(), fn2(),
- fn2(), fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
+ fn2(),
])
})
})
diff --git a/packages/@uppy/utils/src/Translator.test.js b/packages/@uppy/utils/src/Translator.test.ts
similarity index 69%
rename from packages/@uppy/utils/src/Translator.test.js
rename to packages/@uppy/utils/src/Translator.test.ts
index dc8d62e95..e3bb67239 100644
--- a/packages/@uppy/utils/src/Translator.test.js
+++ b/packages/@uppy/utils/src/Translator.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
-import Translator from './Translator.js'
+import Translator, { type Locale } from './Translator.ts'
-const english = {
+const english: Locale<0 | 1> = {
strings: {
chooseFile: 'Choose a file',
youHaveChosen: 'You have chosen: %{fileName}',
@@ -9,12 +9,12 @@ const english = {
0: '%{smart_count} file selected',
1: '%{smart_count} files selected',
},
- pluralize (n) {
- if (n === 1) {
- return 0
- }
- return 1
- },
+ },
+ pluralize(n) {
+ if (n === 1) {
+ return 0
+ }
+ return 1
},
}
@@ -28,7 +28,7 @@ const russian = {
2: 'Выбрано %{smart_count} файлов',
},
},
- pluralize (n) {
+ pluralize(n: number) {
if (n % 10 === 1 && n % 100 !== 11) {
return 0
}
@@ -50,41 +50,58 @@ describe('Translator', () => {
it('should translate a string with non-string elements', () => {
const translator = new Translator({
+ pluralize: english.pluralize,
strings: {
test: 'Hello %{who}!',
test2: 'Hello %{who}',
},
})
- const who = Symbol('who')
- expect(translator.translateArray('test', { who })).toEqual(['Hello ', who, '!'])
+ const who = Symbol('who') as any as string
+ expect(translator.translateArray('test', { who })).toEqual([
+ 'Hello ',
+ who,
+ '!',
+ ])
// No empty string at the end.
- expect(translator.translateArray('test2', { who })).toEqual(['Hello ', who])
+ expect(translator.translateArray('test2', { who })).toEqual([
+ 'Hello ',
+ who,
+ ])
})
})
describe('translation strings inheritance / overriding', () => {
const launguagePackLoadedInCore = english
const defaultStrings = {
+ pluralize: english.pluralize,
strings: {
youHaveChosen: 'You have chosen 123: %{fileName}',
},
}
const userSuppliedStrings = {
+ pluralize: english.pluralize,
strings: {
youHaveChosen: 'Beep boop: %{fileName}',
},
}
it('should prioritize language pack strings from Core over default', () => {
- const translator = new Translator([defaultStrings, launguagePackLoadedInCore])
+ const translator = new Translator([
+ defaultStrings,
+ launguagePackLoadedInCore,
+ ])
expect(
translator.translate('youHaveChosen', { fileName: 'img.jpg' }),
).toEqual('You have chosen: img.jpg')
})
it('should prioritize user-supplied strings over language pack from Core', () => {
- const translator = new Translator([defaultStrings, launguagePackLoadedInCore, userSuppliedStrings])
+ const translator = new Translator([
+ defaultStrings,
+ launguagePackLoadedInCore,
+ userSuppliedStrings,
+ ])
expect(
translator.translate('youHaveChosen', { fileName: 'img.jpg' }),
).toEqual('Beep boop: img.jpg')
@@ -103,17 +120,17 @@ describe('Translator', () => {
describe('pluralization', () => {
it('should translate a string', () => {
const translator = new Translator(russian)
- expect(
- translator.translate('filesChosen', { smart_count: 18 }),
- ).toEqual('Выбрано 18 файлов')
+ expect(translator.translate('filesChosen', { smart_count: 18 })).toEqual(
+ 'Выбрано 18 файлов',
+ )
- expect(
- translator.translate('filesChosen', { smart_count: 1 }),
- ).toEqual('Выбран 1 файл')
+ expect(translator.translate('filesChosen', { smart_count: 1 })).toEqual(
+ 'Выбран 1 файл',
+ )
- expect(
- translator.translate('filesChosen', { smart_count: 0 }),
- ).toEqual('Выбрано 0 файлов')
+ expect(translator.translate('filesChosen', { smart_count: 0 })).toEqual(
+ 'Выбрано 0 файлов',
+ )
})
it('should support strings without plural forms', () => {
@@ -124,12 +141,12 @@ describe('Translator', () => {
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: 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')
@@ -143,11 +160,14 @@ describe('Translator', () => {
1: '%{smart_count} tests',
},
},
+ pluralize: () => 1,
})
expect(() => {
translator.translate('test')
- }).toThrow('Attempted to use a string with plural forms, but no value was given for %{smart_count}')
+ }).toThrow(
+ 'Attempted to use a string with plural forms, but no value was given for %{smart_count}',
+ )
})
})
})
diff --git a/packages/@uppy/utils/src/Translator.js b/packages/@uppy/utils/src/Translator.ts
similarity index 67%
rename from packages/@uppy/utils/src/Translator.js
rename to packages/@uppy/utils/src/Translator.ts
index 8acecb8b3..34a4f202a 100644
--- a/packages/@uppy/utils/src/Translator.js
+++ b/packages/@uppy/utils/src/Translator.ts
@@ -1,7 +1,23 @@
-import has from './hasProperty.js'
+import has from './hasProperty.ts'
-function insertReplacement (source, rx, replacement) {
- const newParts = []
+// We're using a generic because languages have different plural rules.
+export interface Locale {
+ strings: Record>
+ pluralize: (n: number) => T
+}
+
+type Options = {
+ smart_count?: number
+} & {
+ [key: string]: string | number
+}
+
+function insertReplacement(
+ source: Array,
+ rx: RegExp,
+ replacement: string,
+): Array {
+ const newParts: Array = []
source.forEach((chunk) => {
// When the source contains multiple placeholders for interpolation,
// we should ignore chunks that are not strings, because those
@@ -32,14 +48,16 @@ function insertReplacement (source, rx, replacement) {
* @license https://github.com/airbnb/polyglot.js/blob/master/LICENSE
* taken from https://github.com/airbnb/polyglot.js/blob/master/lib/polyglot.js#L299
*
- * @param {string} phrase that needs interpolation, with placeholders
- * @param {object} options with values that will be used to replace placeholders
- * @returns {any[]} interpolated
+ * @param phrase that needs interpolation, with placeholders
+ * @param options with values that will be used to replace placeholders
*/
-function interpolate (phrase, options) {
+function interpolate(
+ phrase: string,
+ options?: Options,
+): Array {
const dollarRegex = /\$/g
const dollarBillsYall = '$$$$'
- let interpolated = [phrase]
+ let interpolated: Array = [phrase]
if (options == null) return interpolated
@@ -55,7 +73,11 @@ function interpolate (phrase, options) {
// We create a new `RegExp` each time instead of using a more-efficient
// string replace so that the same argument can be replaced multiple times
// in the same phrase.
- interpolated = insertReplacement(interpolated, new RegExp(`%\\{${arg}\\}`, 'g'), replacement)
+ interpolated = insertReplacement(
+ interpolated,
+ new RegExp(`%\\{${arg}\\}`, 'g'),
+ replacement as string,
+ )
}
}
@@ -74,13 +96,12 @@ function interpolate (phrase, options) {
* Usage example: `translator.translate('files_chosen', {smart_count: 3})`
*/
export default class Translator {
- /**
- * @param {object|Array