Skip to content

Commit

Permalink
⭐ new: i18n custom block updating
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed Aug 13, 2019
1 parent 00c1890 commit ff236a8
Show file tree
Hide file tree
Showing 7 changed files with 414 additions and 9 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"debug": "^4.1.1",
"js-yaml": "^3.13.1",
"json5": "^2.1.0",
"prettier": "^1.18.2",
"vue-template-compiler": "^2.6.10"
},
"devDependencies": {
Expand All @@ -22,6 +23,7 @@
"@types/js-yaml": "^3.12.1",
"@types/json5": "^0.0.30",
"@types/node": "^12.6.8",
"@types/prettier": "^1.18.1",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"@typescript-eslint/typescript-estree": "^1.13.0",
Expand Down
127 changes: 125 additions & 2 deletions src/infuser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,127 @@
import { LocaleMessages, LocaleMessageMeta } from '../types'
import { SFCDescriptor, SFCBlock } from 'vue-template-compiler'
import { Locale, LocaleMessages, SFCFileInfo } from '../types'

export default function infuse (messages: LocaleMessages, meta: LocaleMessageMeta[]): void {
import { reflectSFCDescriptor, parseContent, stringfyContent } from './utils'
import prettier from 'prettier'

import { debug as Debug } from 'debug'
const debug = Debug('vue-i18n-locale-message:infuser')

export default function infuse (basePath: string, sources: SFCFileInfo[], messages: LocaleMessages): SFCFileInfo[] {
const descriptors = reflectSFCDescriptor(basePath, sources)
const locales = Object.keys(messages)

return descriptors.map(descriptor => {
return {
content: generate(locales, messages, descriptor),
path: descriptor.contentPath
} as SFCFileInfo
})
}

function generate (locales: Locale[], messages: LocaleMessages, descriptor: SFCDescriptor): string {
const target = getTargetLocaleMessages(locales, messages, descriptor)
debug('target locale messages\n', target)

const blocks: SFCBlock[] = getBlocks(descriptor)
blocks.forEach(b => debug(`block: type=${b.type}, start=${b.start}, end=${b.end}`))

const { raw } = descriptor
const content = buildContent(target, raw, blocks)
debug(`build content:\n${content}`)
debug(`content size: raw=${raw.length}, content=${content.length}`)

return format(content, 'vue')
}

function getTargetLocaleMessages (locales: Locale[], messages: LocaleMessages, descriptor: SFCDescriptor): LocaleMessages {
return locales.reduce((target, locale) => {
const obj = messages[locale]
if (obj) {
let o: any = obj
const hierarchy = descriptor.hierarchy.concat()
while (hierarchy.length > 0) {
const key = hierarchy.shift()
if (!key) { break }
o = o[key]
}
return Object.assign(target, { [locale]: o }) as LocaleMessages
} else {
return target
}
}, {} as LocaleMessages)
}

function getBlocks (descriptor: SFCDescriptor): SFCBlock[] {
const { template, script, styles, customBlocks } = descriptor
const blocks: SFCBlock[] = [...styles, ...customBlocks]
template && blocks.push(template as SFCBlock)
script && blocks.push(script as SFCBlock)
blocks.sort((a, b) => { return (a.start as number) - (b.start as number) })
return blocks
}

function buildContent (target: LocaleMessages, raw: string, blocks: SFCBlock[]): string {
let offset = 0
let contents: string[] = []
let targetLocales = Object.keys(target) as Locale[]

contents = blocks.reduce((contents, block) => {
if (block.type === 'i18n') {
let lang = block.attrs.lang
lang = (!lang || typeof lang !== 'string') ? 'json' : lang

let messages: any = null
const locale = block.attrs.locale as Locale
if (!locale || typeof locale !== 'string') {
const obj = parseContent(block.content, lang)
const locales = Object.keys(obj) as Locale[]
messages = locales.reduce((messages, locale) => {
return Object.assign(messages, { [locale]: target[locale] })
}, {} as LocaleMessages)
locales.forEach(locale => {
targetLocales = targetLocales.filter(l => l !== locale)
})
} else {
messages = Object.assign({}, target[locale])
targetLocales = targetLocales.filter(l => l !== locale)
}

contents = contents.concat(raw.slice(offset, block.start))
const serialized = `\n${format(stringfyContent(messages, lang), lang)}`
contents = contents.concat(serialized)
offset = block.end as number
} else {
contents = contents.concat(raw.slice(offset, block.end))
offset = block.end as number
}
return contents
}, contents)
contents = contents.concat(raw.slice(offset, raw.length))

if (targetLocales.length > 0) {
contents = targetLocales.reduce((contents, locale) => {
contents.push(`\n
<i18n locale="${locale}">
${format(stringfyContent(target[locale], 'json'), 'json')}</i18n>`)
return contents
}, contents)
}

return contents.join('')
}

function format (source: string, lang: string): string {
switch (lang) {
case 'vue':
return prettier.format(source, { parser: 'vue' })
case 'yaml':
case 'yml':
return prettier.format(source, { parser: 'yaml', tabWidth: 2 })
case 'json5':
return prettier.format(source, { parser: 'json5', tabWidth: 2 })
case 'json':
default:
return prettier.format(source, { parser: 'json-stringify', tabWidth: 2 })
}
}
4 changes: 3 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ export function stringfyContent (content: any, lang: string): string {
switch (lang) {
case 'yaml':
case 'yml':
return yaml.safeDump(content)
const s = yaml.safeDump(content, { indent: 20 })
debug('yaml:', s)
return s
case 'json5':
return JSON5.stringify(content, null, 2)
case 'json':
Expand Down
156 changes: 156 additions & 0 deletions test/__snapshots__/infuser.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`json: /path/to/project1/src/App.vue 1`] = `
"<template>
<p>template</p>
</template>
<script>
export default {};
</script>
<i18n>
{
\\"en\\": {
\\"title\\": \\"Application\\"
},
\\"ja\\": {
\\"title\\": \\"アプリケーション\\"
}
}
</i18n>
"
`;

exports[`json: /path/to/project1/src/components/Modal.vue 1`] = `
"<template>
<p>template</p>
</template>
<script>
export default {};
</script>
<i18n locale=\\"en\\">
{
\\"ok\\": \\"OK\\",
\\"cancel\\": \\"Cancel\\"
}
</i18n>
<i18n locale=\\"ja\\">
{
\\"ok\\": \\"OK\\",
\\"cancel\\": \\"キャンセル\\"
}
</i18n>
"
`;
exports[`json: /path/to/project1/src/components/nest/RankingTable.vue 1`] = `
"<template>
<p>template</p>
</template>
<script>
export default {};
</script>
<i18n locale=\\"en\\">
{
\\"headers\\": {
\\"rank\\": \\"Rank\\",
\\"name\\": \\"Name\\",
\\"score\\": \\"Score\\"
}
}
</i18n>
<i18n locale=\\"ja\\">
{
\\"headers\\": {
\\"rank\\": \\"ランク\\",
\\"name\\": \\"名前\\",
\\"score\\": \\"スコア\\"
}
}
</i18n>
"
`;
exports[`json: /path/to/project1/src/pages/Login.vue 1`] = `
"<template>
<p>template</p>
</template>
<script>
export default {};
</script>
<i18n>
{
\\"ja\\": {
\\"id\\": \\"ユーザーID\\",
\\"passowrd\\": \\"パスワード\\",
\\"confirm\\": \\"パスワードの確認入力\\",
\\"button\\": \\"ログイン\\"
}
}
</i18n>
<i18n locale=\\"en\\">
{
\\"id\\": \\"User ID\\",
\\"password\\": \\"Password\\",
\\"confirm\\": \\"Confirm Password\\",
\\"button\\": \\"Login\\"
}
</i18n>
"
`;
exports[`json5: /path/to/project1/src/components/Modal.vue 1`] = `
"<template>
<p>template</p>
</template>
<script>
export default {};
</script>
<i18n lang=\\"json5\\" locale=\\"en\\">
{
ok: \\"OK\\",
cancel: \\"Cancel\\"
}
</i18n>
<i18n locale=\\"ja\\">
{
\\"ok\\": \\"OK\\",
\\"cancel\\": \\"キャンセル\\"
}
</i18n>
"
`;
exports[`yaml: /path/to/project1/src/components/Modal.vue 1`] = `
"<template>
<p>template</p>
</template>
<script>
export default {};
</script>
<i18n lang=\\"yaml\\" locale=\\"en\\">
ok: OK
cancel: Cancel
</i18n>
<i18n lang=\\"yml\\" locale=\\"ja\\">
ok: OK
cancel: キャンセル
</i18n>
"
`;
8 changes: 4 additions & 4 deletions test/fixtures/file/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ export default {}
</script>
<i18n lang="yaml" locale="en">
ok: OK
cancel: Cancel
ok: OK
cancel: Cancel
</i18n>
<i18n lang="yml" locale="ja">
ok: OK
cancel: キャンセル
ok: OK
cancel: キャンセル
</i18n>`
}]

0 comments on commit ff236a8

Please sign in to comment.