Skip to content

Commit

Permalink
#138 Fallback Locale as array for cascading fallbacks (#829)
Browse files Browse the repository at this point in the history
* - adds tests for fallbacks to be implemented (#2)

* adds tests for fallbacks to be implemented (#2)

* implementation for complex fallback variants (#2)

* fixed test errors (#2)

* fixed some documentation (#2)

* fixes fallback test (#2)

* refactor fallback tests (#2)

* allow fallbackLocale to be false explicit (#2)

* fixing test message (#2)

* fixing use of wrong variable and type

* Update src/index.js - according suggestion :+1

Co-Authored-By: kazuya kawaguchi <kawakazu80@gmail.com>

Co-authored-by: kazuya kawaguchi <kawakazu80@gmail.com>
  • Loading branch information
mmokross and kazupon committed Apr 11, 2020
1 parent 9b2fd11 commit 6ad455f
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 14 deletions.
4 changes: 2 additions & 2 deletions gitbook/en/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,15 @@ You can specify the below some options of `I18nOptions` constructor options of [

- **Default:** `'en-US'`

The locale of localization.
The locale of localization. If the locale contains a territory and a dialect, this locale contains an implicit fallback.

#### fallbackLocale

- **Type:** `Locale`

- **Default:** `'en-US'`

The locale of fallback localization.
The locale of fallback localization. For more complex fallback definitions see fallback.

#### messages

Expand Down
54 changes: 54 additions & 0 deletions gitbook/en/fallback.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Fallback localization

## Implicit fallback using locales

If a `locale` is given containing a territory and an optional dialect, the implicit fallback is activated automatically.

For example `de-DE-bavarian` would fallback
1. `de-DE-bavarian`
1. `de-DE`
1. `de`

To supress the automatic fallback, add the postfix exclamation mark `!`, for example `de-DE!`


## Explicit fallback with one locale

When the `message` key does not exist in the `ja` locale:

```javascript
Expand Down Expand Up @@ -33,3 +47,43 @@ The following will be the output:
```html
<p>hello world</p>
```


## Explicit fallback with an array of locales

It is possible to set more than one fallback locale by using an array of locales. For example

```javascript
fallbackLocale: [ 'fr', en' ],
```
## Explicit fallback with decision maps
If more complex decision maps for fallback locales are required, it is possible to define decision maps with according fallback locales.
Using the following decision map
```javascript
fallbackLocale: {
/* 1 */ 'de-CH': ['fr', 'it'],
/* 2 */ 'zh-Hant': ['zh-Hans'],
/* 3 */ 'es-CL': ['es-AR'],
/* 4 */ 'es': ['en-GB'],
/* 5 */ 'pt': ['es-AR'],
/* 6 */ 'default': ['en', 'da']
},
```
will result in the following fallback chains
| locale | fallback chains |
|--------|-----------------|
| `'de-CH'` | de-CH > fr > it > en > da |
| `'de'` | de > en > da |
| `'zh-Hant'` | zh-Hant > zh-Hans > zh > en > da |
| `'es-SP'` | es-SP > es > en-GB > en > da |
| `'es-SP!'` | es-SP > en > da |
| `'fr'` | fr > en > da |
| `'pt-BR'` | pt-BR > pt > es-AR > es > en-GB > en > da |
| `'es-CL'` | es-CL > es-AR > es > en-GB > en > da |
142 changes: 130 additions & 12 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default class VueI18n {
_root: any
_sync: boolean
_fallbackRoot: boolean
_localeChainCache: Map<string, Array<Locale>>
_missing: ?MissingHandler
_exist: Function
_silentTranslationWarn: boolean | RegExp
Expand All @@ -67,7 +68,9 @@ export default class VueI18n {
}

const locale: Locale = options.locale || 'en-US'
const fallbackLocale: Locale = options.fallbackLocale || 'en-US'
const fallbackLocale: any = options.fallbackLocale === false
? false
: options.fallbackLocale || 'en-US'
const messages: LocaleMessages = options.messages || {}
const dateTimeFormats = options.dateTimeFormats || {}
const numberFormats = options.numberFormats || {}
Expand Down Expand Up @@ -234,6 +237,7 @@ export default class VueI18n {

get fallbackLocale (): Locale { return this._vm.fallbackLocale }
set fallbackLocale (locale: Locale): void {
this._localeChainCache = new Map()
this._vm.$set(this._vm, 'fallbackLocale', locale)
}

Expand Down Expand Up @@ -456,6 +460,119 @@ export default class VueI18n {
return interpolateMode === 'string' && typeof ret !== 'string' ? ret.join('') : ret
}

resetFallbackLocale (
fallbackLocale: any
) {
this._localeChainCache = new Map()
this.fallbackLocale = fallbackLocale
}

_appendItemToChain (
chain: Array<Locale>,
item: Locale,
blocks: any
): any {
var follow = false
if (!chain.includes(item)) {
follow = true
if (item) {
follow = !item.endsWith('!')
item = item.replace(/!/g, '')
chain.push(item)
if (blocks && blocks[item]) {
follow = blocks[item]
}
}
}
return follow
}

_appendLocaleToChain (
chain: Array<Locale>,
locale: Locale,
blocks: any
): any {
var follow
var tokens = locale.split('-')
do {
var item = tokens.join('-')
follow = this._appendItemToChain(chain, item, blocks)
tokens.splice(-1, 1)
} while (tokens.length && (follow === true))
return follow
}

_appendBlockToChain (
chain: Array<Locale>,
block: Array<Locale>,
blocks: any
): any {
var follow = true
for (var i = 0; (i < block.length) && (typeof follow === 'boolean'); i++) {
var locale = block[i]
follow = this._appendLocaleToChain(chain, locale, blocks)
}
return follow
}

getLocaleChain (
start: Locale,
fallbackLocale: any
): Array<Locale> {
if (start === '--') {
return []
}
//
if (!this._localeChainCache) {
this._localeChainCache = new Map()
}
var chain = this._localeChainCache.get(start)
if (!chain) {
if (!fallbackLocale) {
fallbackLocale = this.fallbackLocale
}
chain = []
// first block defined by start
var block = [start]
// while any intervening block found
while (Array.isArray(block)) {
block = this._appendBlockToChain(
chain,
block,
fallbackLocale
)
}
// last block defined by default
var defaults
if (Array.isArray(fallbackLocale)) {
defaults = fallbackLocale
} else if (fallbackLocale instanceof Object) {
if (fallbackLocale['default']) {
defaults = fallbackLocale['default']
} else {
defaults = null
}
} else {
defaults = fallbackLocale
}
// convert defaults to array
if (typeof defaults === 'string') {
block = [defaults]
} else {
block = defaults
}
if (block) {
this._appendBlockToChain(
chain,
block,
null
)
}
this._localeChainCache.set(start, chain)
}
return chain
}

_translate (
messages: LocaleMessages,
locale: Locale,
Expand All @@ -465,19 +582,20 @@ export default class VueI18n {
interpolateMode: string,
args: any
): any {
let res: any =
this._interpolate(locale, messages[locale], key, host, interpolateMode, args, [key])
if (!isNull(res)) { return res }

res = this._interpolate(fallback, messages[fallback], key, host, interpolateMode, args, [key])
if (!isNull(res)) {
if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
warn(`Fall back to translate the keypath '${key}' with '${fallback}' locale.`)
var chain = this.getLocaleChain(locale, fallback)
var res
for (var i = 0; i < chain.length; i++) {
var step = chain[i]
res =
this._interpolate(step, messages[step], key, host, interpolateMode, args, [key])
if (!isNull(res)) {
if (step !== locale && process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
warn(("Fall back to translate the keypath '" + key + "' with '" + step + "' locale."))
}
return res
}
return res
} else {
return null
}
return null
}

_t (key: Path, _locale: Locale, messages: LocaleMessages, host: any, ...values: any): any {
Expand Down
80 changes: 80 additions & 0 deletions test/unit/fallback.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import messages from './fixture/index'

describe('kazupon#138 mmokross#2 - Fallback Locale as array for cascading fallbacks ...', () => {
var types = [
{
description: '... none',
fallbackLocale: false,
tests: [
{ description: 'English', locale: 'en', expected: ['en'] },
{ description: 'English (Great Britain)', locale: 'en-GB', expected: ['en-GB', 'en'] },
{ description: 'German', locale: 'de', expected: ['de'] },
{ description: 'German (Switzerland)', locale: 'de-CH', expected: ['de-CH', 'de'] }
]
},
{
description: '... simple',
fallbackLocale: 'en',
tests: [
{ description: 'English', locale: 'en', expected: ['en'] },
{ description: 'English (Great Britain)', locale: 'en-GB', expected: ['en-GB', 'en'] },
{ description: 'German', locale: 'de', expected: ['de', 'en'] },
{ description: 'German (Switzerland)', locale: 'de-CH', expected: ['de-CH', 'de', 'en'] }
]
},
{
description: '... array',
fallbackLocale: ['en', 'ja'],
tests: [
{ description: 'English', locale: 'en', expected: ['en', 'ja'] },
{ description: 'English (Great Britain)', locale: 'en-GB', expected: ['en-GB', 'en', 'ja'] },
{ description: 'German', locale: 'de', expected: ['de', 'en', 'ja'] },
{ description: 'German (Switzerland)', locale: 'de-CH', expected: ['de-CH', 'de', 'en', 'ja'] },
{ description: 'Japanese', locale: 'ja', expected: ['ja', 'en'] }
]
},
{
description: '... complex',
fallbackLocale: {
'de-CH': ['fr', 'it'],
'zh-Hant': ['zh-Hans'],
'es-CL': ['es-AR'],
'es': ['en-GB'],
'pt': ['es-AR'],
'default': ['en', 'da']
},
tests: [
{ description: 'German (Switzerland)', locale: 'de-CH', expected: ['de-CH', 'fr', 'it', 'en', 'da'] },
{ description: 'German (Switzerland) EXACT', locale: 'de-CH!', expected: ['de-CH', 'fr', 'it', 'en', 'da'] },
{ description: 'German', locale: 'de', expected: ['de', 'en', 'da'] },
{ description: 'Traditional Chinese', locale: 'zh-Hant', expected: ['zh-Hant', 'zh-Hans', 'zh', 'en', 'da'] },
{ description: 'Spanish (Spain)', locale: 'es-SP', expected: ['es-SP', 'es', 'en-GB', 'en', 'da'] },
{ description: 'Spanish (Spain) EXACT', locale: 'es-SP!', expected: ['es-SP', 'en', 'da'] },
{ description: 'French', locale: 'fr', expected: ['fr', 'en', 'da'] },
{ description: 'Portuguese (Brazil)', locale: 'pt-BR', expected: ['pt-BR', 'pt', 'es-AR', 'es', 'en-GB', 'en', 'da'] },
{ description: 'Spanish (Chile)', locale: 'es-CL', expected: ['es-CL', 'es-AR', 'es', 'en-GB', 'en', 'da'] }
]
}
]
types.forEach(function (type) {
describe(type.description, () => {
let i18n
beforeEach(() => {
i18n = new VueI18n({
locale: 'en',
fallbackLocale: type.fallbackLocale,
messages,
modifiers: {
custom: str => str.replace(/[aeiou]/g, 'x')
}
})
})
type.tests.forEach(function (test) {
it(test.description + ': ' + test.locale + ' should fallback to ' + test.expected, () => {
// console.log(test.locale + ': --> ' + i18n.getLocaleChain(test.locale))
assert.deepEqual(i18n.getLocaleChain(test.locale), test.expected)
})
})
})
})
})

0 comments on commit 6ad455f

Please sign in to comment.