Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#138 Fallback Locale as array for cascading fallbacks #829

Merged
merged 12 commits into from
Apr 11, 2020
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 |
kazupon marked this conversation as resolved.
Show resolved Hide resolved
| `'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 @@ -66,7 +67,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 @@ -232,6 +235,7 @@ export default class VueI18n {

get fallbackLocale (): Locale { return this._vm.fallbackLocale }
set fallbackLocale (locale: Locale): void {
this._vm.$set(this._vm, '_localeChainCache', new Map())
mmokross marked this conversation as resolved.
Show resolved Hide resolved
this._vm.$set(this._vm, 'fallbackLocale', locale)
}

Expand Down Expand Up @@ -451,6 +455,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 @@ -460,19 +577,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)
})
})
})
})
})