diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ec8bc9..6c0b4cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [next-version] - next-release-date
+### Added
+* Localization support.
+
 ### Changed
 * Switched to Typescript.
 
diff --git a/README.md b/README.md
index 3517700..219ac0d 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,20 @@ const schema = yup.string().min(6).minUppercase(3).maxRepeating(2).minWords(2)
 await schema.isValid('Now, THIS is some password.') // => true
 await schema.isValid('But thiiis is not.') // => false
 ```
+Localize your error messages:
+```js
+yup.setLocale({
+    string: {
+        minLowercase: 'Localized message (path=${path};length=${length})',
+        minUppercase: 'Localized message (path=${path};length=${length})',
+        minNumbers: 'Localized message (path=${path};length=${length})',
+        minSymbols: 'Localized message (path=${path};length=${length})',
+        maxRepeating: 'Localized message (path=${path};length=${length})',
+        minWords: 'Localized message (path=${path};length=${length})',
+    }, // when using typescript, you may want to append `as any` to the end
+       // of this object to avoid type errors.
+})
+```
 
 ## API
 
diff --git a/examples/localization.ts b/examples/localization.ts
new file mode 100644
index 0000000..3750579
--- /dev/null
+++ b/examples/localization.ts
@@ -0,0 +1,29 @@
+/* eslint-disable no-console, no-template-curly-in-string */
+import * as yup from 'yup'
+import YupPassword from '../src'
+YupPassword(yup)
+
+// StringLocale declaration merging does not seem to
+// work, so we have to declare yup password's locale
+// overrides as a separate "Record" object.
+const locale: Record<string, any> = {
+    minSymbols: 'This is now localized. path=${path};length=${length}',
+}
+
+yup.setLocale({
+    string: {
+        ...locale,
+        // Add other messages here
+    },
+})
+
+const schema = yup.object({
+    password: yup.string().password(),
+})
+
+const input = {
+    password: 'Password1',
+}
+
+schema.validate(input, { abortEarly: false })
+    .catch(e => console.error(e.errors))
diff --git a/src/index.ts b/src/index.ts
index 366f112..2c3403b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,19 +1,22 @@
-/* eslint-disable @typescript-eslint/method-signature-style, no-template-curly-in-string */
+/* eslint-disable
+   @typescript-eslint/restrict-template-expressions,
+   @typescript-eslint/method-signature-style,
+   no-template-curly-in-string
+*/
 
-// NOTES.
-// 1. error message pluralization will have to go away to support localization (reason: yup limitation)
-// 2. (breaking) in a future release setup will be immediately invoked and the export will be removed
-
-// pluralize
-function p (word: string, num: number): string {
-    return num === 1 ? word : `${word}s`
-}
-
-function isNullOrUndefined (value: any): value is null | undefined {
-    return value === null || value === undefined
-}
+import type { Message } from 'yup'
 
 declare module 'yup' {
+    // for declaration merging to work, StringLocale prob needs to be exported from yup
+    interface StringLocale {
+        // ..rename params from length to min, max, etc?
+        minLowercase?: Message<{ length: number }>
+        minUppercase?: Message<{ length: number }>
+        minNumbers?: Message<{ length: number }>
+        minSymbols?: Message<{ length: number }>
+        maxRepeating?: Message<{ length: number }>
+        minWords?: Message<{ length: number }>
+    }
     interface StringSchema {
         minLowercase (length?: number, message?: string): StringSchema
         minUppercase (length?: number, message?: string): StringSchema
@@ -25,13 +28,33 @@ declare module 'yup' {
     }
 }
 
-export default function setup (yup: typeof import('yup')): void {
-    yup.addMethod(yup.string, 'minLowercase', function minLowercase (length: number = 1, message?: string) {
-        const msg = message || '${path} must contain at least ${length} lowercase ' + p('letter', length)
+// pluralize
+function p (word: string, num: number): string {
+    return num === 1 ? word : `${word}s`
+}
+
+const messages: Record<string, any> = {
+    minLowercase: ({ path, length }) => `${path} must contain at least ${length} lowercase ${p('letter', length)}`,
+    minUppercase: ({ path, length }) => `${path} must contain at least ${length} uppercase ${p('letter', length)}`,
+    minNumbers: ({ path, length }) => `${path} must contain at least ${length} ${p('number', length)}`,
+    minSymbols: ({ path, length }) => `${path} must contain at least ${length} ${p('symbol', length)}`,
+    maxRepeating: ({ path, length }) => `${path} must not contain sequences of more than ${length} repeated characters`,
+    minWords: ({ path, length }) => `${path} must contain at least ${length} ${p('word', length)}`,
+}
+
+function isNullOrUndefined (value: any): value is null | undefined {
+    return value === null || value === undefined
+}
+
+export default function setup ({ setLocale, defaultLocale, addMethod, string }: typeof import('yup')): void {
+    setLocale({ string: messages })
+    const locale = defaultLocale.string as typeof messages
+
+    addMethod(string, 'minLowercase', function minLowercase (length: number = 1, message: string = locale?.minLowercase) {
         return this.test({
+            message,
             name: 'minLowercase',
             exclusive: true,
-            message: msg,
             params: { length },
             test (value) {
                 return isNullOrUndefined(value) || (value.match(/[a-z]/g) || []).length >= length
@@ -39,12 +62,11 @@ export default function setup (yup: typeof import('yup')): void {
         })
     })
 
-    yup.addMethod(yup.string, 'minUppercase', function minUppercase (length: number = 1, message?: string) {
-        const msg = message || '${path} must contain at least ${length} uppercase ' + p('letter', length)
+    addMethod(string, 'minUppercase', function minUppercase (length: number = 1, message: string = locale?.minUppercase) {
         return this.test({
+            message,
             name: 'minUppercase',
             exclusive: true,
-            message: msg,
             params: { length },
             test (value) {
                 return isNullOrUndefined(value) || (value.match(/[A-Z]/g) || []).length >= length
@@ -52,12 +74,11 @@ export default function setup (yup: typeof import('yup')): void {
         })
     })
 
-    yup.addMethod(yup.string, 'minNumbers', function minNumbers (length: number = 1, message?: string) {
-        const msg = message || '${path} must contain at least ${length} ' + p('number', length)
+    addMethod(string, 'minNumbers', function minNumbers (length: number = 1, message: string = locale?.minNumbers) {
         return this.test({
+            message,
             name: 'minNumbers',
             exclusive: true,
-            message: msg,
             params: { length },
             test (value) {
                 return isNullOrUndefined(value) || (value.match(/[0-9]/g) || []).length >= length
@@ -65,12 +86,11 @@ export default function setup (yup: typeof import('yup')): void {
         })
     })
 
-    yup.addMethod(yup.string, 'minSymbols', function minSymbols (length: number = 1, message?: string) {
-        const msg = message || '${path} must contain at least ${length} ' + p('symbol', length)
+    addMethod(string, 'minSymbols', function minSymbols (length: number = 1, message: string = locale?.minSymbols) {
         return this.test({
+            message,
             name: 'minSymbols',
             exclusive: true,
-            message: msg,
             params: { length },
             test (value) {
                 return isNullOrUndefined(value) || (value.match(/[^a-zA-Z0-9\s]/g) || []).length >= length
@@ -78,12 +98,11 @@ export default function setup (yup: typeof import('yup')): void {
         })
     })
 
-    yup.addMethod(yup.string, 'maxRepeating', function maxRepeating (length: number = 2, message?: string) {
-        const msg = message || '${path} must not contain sequences of more than ${length} repeated ' + p('character', length)
+    addMethod(string, 'maxRepeating', function maxRepeating (length: number = 2, message: string = locale?.maxRepeating) {
         return this.test({
+            message,
             name: 'maxRepeating',
             exclusive: true,
-            message: msg,
             params: { length },
             test (value) {
                 return isNullOrUndefined(value) || ! new RegExp(`(.)\\1{${length},}`).test(value)
@@ -91,22 +110,19 @@ export default function setup (yup: typeof import('yup')): void {
         })
     })
 
-    yup.addMethod(yup.string, 'minWords', function minWords (length: number = 2, message?: string) {
-        const msg = message || '${path} must contain at least ${length} ' + p('word', length)
-        // eslint-disable-next-line prefer-regex-literals
-        const rx = new RegExp('[a-zA-Z0-9]')
+    addMethod(string, 'minWords', function minWords (length: number = 2, message: string = locale?.minWords) {
         return this.test({
+            message,
             name: 'minWords',
             exclusive: true,
-            message: msg,
             params: { length },
             test (value) {
-                return isNullOrUndefined(value) || value.split(' ').filter(v => !! v && rx.test(v)).length >= length
+                return isNullOrUndefined(value) || value.split(' ').filter(v => !! v && /[a-zA-Z0-9]/.test(v)).length >= length
             },
         })
     })
 
-    yup.addMethod(yup.string, 'password', function password () {
+    addMethod(string, 'password', function password () {
         return this
             .min(8)
             .max(250)
diff --git a/tests/locale.spec.ts b/tests/locale.spec.ts
new file mode 100644
index 0000000..797fcf4
--- /dev/null
+++ b/tests/locale.spec.ts
@@ -0,0 +1,118 @@
+/* eslint-disable
+   no-template-curly-in-string
+*/
+
+import * as yup from 'yup'
+import setup from '../src'
+setup(yup)
+const schema = yup.string()
+
+describe('Locale support', () => {
+    describe('test default message: singular', () => {
+        test('minLowercase()', async () => {
+            const errorMessage = await schema.minLowercase(1).validate('A').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 1 lowercase letter')
+        })
+
+        test('minUppercase()', async () => {
+            const errorMessage = await schema.minUppercase(1).validate('a').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 1 uppercase letter')
+        })
+
+        test('minNumbers()', async () => {
+            const errorMessage = await schema.minNumbers(1).validate('a').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 1 number')
+        })
+
+        test('minSymbols()', async () => {
+            const errorMessage = await schema.minSymbols(1).validate('a').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 1 symbol')
+        })
+
+        test('maxRepeating()', async () => {
+            const errorMessage = await schema.maxRepeating(1).validate('aa').catch(e => e.message)
+            expect(errorMessage).toBe('this must not contain sequences of more than 1 repeated characters')
+        })
+
+        test('minWords()', async () => {
+            const errorMessage = await schema.minWords(1).validate('$').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 1 word')
+        })
+    }) // group
+
+    describe('test default message: plural', () => {
+        test('minLowercase()', async () => {
+            const errorMessage = await schema.minLowercase(2).validate('a').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 2 lowercase letters')
+        })
+
+        test('minUppercase()', async () => {
+            const errorMessage = await schema.minUppercase(2).validate('A').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 2 uppercase letters')
+        })
+
+        test('minNumbers()', async () => {
+            const errorMessage = await schema.minNumbers(2).validate('1').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 2 numbers')
+        })
+
+        test('minSymbols()', async () => {
+            const errorMessage = await schema.minSymbols(2).validate('!').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 2 symbols')
+        })
+
+        test('maxRepeating()', async () => {
+            const errorMessage = await schema.maxRepeating(2).validate('aaa').catch(e => e.message)
+            expect(errorMessage).toBe('this must not contain sequences of more than 2 repeated characters')
+        })
+
+        test('minWords()', async () => {
+            const errorMessage = await schema.minWords(2).validate('a').catch(e => e.message)
+            expect(errorMessage).toBe('this must contain at least 2 words')
+        })
+    }) // group
+
+    describe('test localized message', () => {
+        beforeAll(() => {
+            yup.setLocale({
+                string: {
+                    minLowercase: 'name=minLowercase;path=${path};length=${length}',
+                    minUppercase: 'name=minUppercase;path=${path};length=${length}',
+                    minNumbers: 'name=minNumbers;path=${path};length=${length}',
+                    minSymbols: 'name=minSymbols;path=${path};length=${length}',
+                    maxRepeating: 'name=maxRepeating;path=${path};length=${length}',
+                    minWords: 'name=minWords;path=${path};length=${length}',
+                } as any,
+            })
+        })
+        test('minLowercase()', async () => {
+            const errorMessage = await schema.minLowercase(2).validate('a').catch(e => e.message)
+            expect(errorMessage).toBe('name=minLowercase;path=this;length=2')
+        })
+
+        test('minUppercase()', async () => {
+            const errorMessage = await schema.minUppercase(2).validate('A').catch(e => e.message)
+            expect(errorMessage).toBe('name=minUppercase;path=this;length=2')
+        })
+
+        test('minNumbers()', async () => {
+            const errorMessage = await schema.minNumbers(2).validate('1').catch(e => e.message)
+            expect(errorMessage).toBe('name=minNumbers;path=this;length=2')
+        })
+
+        test('minSymbols()', async () => {
+            const errorMessage = await schema.minSymbols(2).validate('!').catch(e => e.message)
+            expect(errorMessage).toBe('name=minSymbols;path=this;length=2')
+        })
+
+        test('maxRepeating()', async () => {
+            const errorMessage = await schema.maxRepeating(2).validate('aaa').catch(e => e.message)
+            expect(errorMessage).toBe('name=maxRepeating;path=this;length=2')
+        })
+
+        test('minWords()', async () => {
+            const errorMessage = await schema.minWords(2).validate('a').catch(e => e.message)
+            expect(errorMessage).toBe('name=minWords;path=this;length=2')
+        })
+    }) // group
+}) // group