Skip to content

Commit

Permalink
⚡ improvement: Support meta local message outputting for 3rd vendor t…
Browse files Browse the repository at this point in the history
…ools

closes #13
  • Loading branch information
kazupon committed Sep 20, 2019
1 parent b89268f commit 04dee29
Show file tree
Hide file tree
Showing 11 changed files with 527 additions and 222 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"@vue/component-compiler-utils": "^3.0.0",
"debug": "^4.1.1",
"deep-diff": "^1.0.2",
"glob": "^7.1.4",
"js-yaml": "^3.13.1",
"json5": "^2.1.0",
Expand All @@ -22,6 +23,7 @@
},
"devDependencies": {
"@types/debug": "^4.1.4",
"@types/deep-diff": "^1.0.0",
"@types/glob": "^7.1.1",
"@types/jest": "^24.0.15",
"@types/js-yaml": "^3.12.1",
Expand Down
111 changes: 108 additions & 3 deletions src/commands/infuse.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Arguments, Argv } from 'yargs'

import { resolve, readSFC } from '../utils'
import { resolve, parsePath, readSFC } from '../utils'
import infuse from '../infuser'
import squeeze from '../squeezer'
import fs from 'fs'
import { LocaleMessages, SFCFileInfo } from '../../types'
import { applyDiff } from 'deep-diff'
import { LocaleMessages, SFCFileInfo, MetaLocaleMessage, Locale } from '../../types'

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

type InfuseOptions = {
target: string
Expand Down Expand Up @@ -33,7 +38,11 @@ export const builder = (args: Argv): Argv<InfuseOptions> => {
export const handler = (args: Arguments<InfuseOptions>): void => {
const targetPath = resolve(args.target)
const messagesPath = resolve(args.messages)
const newSources = infuse(targetPath, readSFC(targetPath), readLocaleMessages(messagesPath))
const sources = readSFC(targetPath)
const messages = readLocaleMessages(messagesPath)
const meta = squeeze(targetPath, sources)
apply(messages, meta)
const newSources = infuse(targetPath, sources, meta)
writeSFC(newSources)
}

Expand All @@ -43,6 +52,102 @@ function readLocaleMessages (path: string): LocaleMessages {
return JSON.parse(data) as LocaleMessages
}

function removeItem<T> (item: T, items: T[]): boolean {
const index = items.indexOf(item)
if (index === -1) { return false }
items.splice(index, 1)
return true
}

function apply (messages: LocaleMessages, meta: MetaLocaleMessage): MetaLocaleMessage {
const { target, components } = meta

for (const [component, blocks] of Object.entries(components)) {
debug(`apply component = ${component}, blocks = ${JSON.stringify(blocks)}`)
const { hierarchy } = parsePath(target, component)

const collectMessages = getTargetLocaleMessages(messages, hierarchy)
debug('collect messages', JSON.stringify(collectMessages, null, 2))

const sourceLocales: Locale[] = Object.keys(collectMessages)
const targetLocales = blocks.reduce((locales, block) => {
if (block.locale) {
locales.push(block.locale)
} else {
locales = Object.keys(block.messages).reduce((locales, locale) => {
locales.push(locale)
return locales
}, locales)
}
return locales
}, [] as Locale[])
debug(`locales: source = ${sourceLocales}, target = ${targetLocales}`)

blocks.forEach(block => {
const { locale } = block
if (locale) {
applyDiff(block.messages[locale], collectMessages[locale])
removeItem(locale, sourceLocales)
removeItem(locale, targetLocales)
} else {
const locales: Locale[] = Object.keys(block.messages)
locales.forEach(locale => {
applyDiff(block.messages[locale], collectMessages[locale])
removeItem(locale, sourceLocales)
removeItem(locale, targetLocales)
})
}
})
debug(`locales remain: source = ${sourceLocales}, target = ${targetLocales}`)

if (sourceLocales.length) {
sourceLocales.forEach(locale => {
blocks.push({
lang: 'json',
locale,
messages: Object.assign({}, { [locale]: collectMessages[locale] })
})
})
}

if (targetLocales.length) {
debug('invalid target remain locales ...', targetLocales.length)
}
}

return meta
}

function getTargetLocaleMessages (messages: LocaleMessages, hierarchy: string[]): LocaleMessages {
return Object.keys(messages).reduce((target, locale) => {
debug(`processing curernt: locale=${locale}, target=${JSON.stringify(target)}`)

const obj = messages[locale]
if (obj) {
let o: any = obj
let prev: any = null
const h = hierarchy.concat()
while (h.length > 0) {
const key = h.shift()
debug('processing hierarchy key: ', key)

if (!key || !o) { break }
o = o[key]
prev = o
debug(`processing o = ${JSON.stringify(o)}, prev = ${JSON.stringify(prev)}`)
}

if (!o && !prev) {
return target
} else {
return Object.assign(target, { [locale]: ((!o && prev) ? prev : o) }) as LocaleMessages
}
} else {
return target
}
}, {} as LocaleMessages)
}

function writeSFC (sources: SFCFileInfo[]) {
// TODO: async implementation
sources.forEach(({ path, content }) => {
Expand Down
54 changes: 51 additions & 3 deletions src/commands/squeeze.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Arguments, Argv } from 'yargs'
import { LocaleMessages } from '../../types'
import { LocaleMessages, MetaLocaleMessage, Locale } from '../../types'

import { resolve, readSFC } from '../utils'
import { resolve, parsePath, readSFC } from '../utils'
import squeeze from '../squeezer'
import fs from 'fs'

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

type SqueezeOptions = {
target: string
output: string
Expand Down Expand Up @@ -33,10 +36,55 @@ export const builder = (args: Argv): Argv<SqueezeOptions> => {

export const handler = (args: Arguments<SqueezeOptions>): void => {
const targetPath = resolve(args.target)
const messages = squeeze(targetPath, readSFC(targetPath))
const meta = squeeze(targetPath, readSFC(targetPath))
const messages = generate(meta)
writeLocaleMessages(resolve(args.output), messages)
}

function generate (meta: MetaLocaleMessage): LocaleMessages {
const { target, components } = meta
let messages: LocaleMessages = {}

const assignLocales = (locales: Locale[], messages: LocaleMessages): LocaleMessages => {
return locales.reduce((messages, locale) => {
!messages[locale] && Object.assign(messages, { [locale]: {}})
return messages
}, messages)
}

for (const [component, blocks] of Object.entries(components)) {
debug(`generate component = ${component}`)
const parsed = parsePath(target, component)
messages = blocks.reduce((messages, block) => {
debug(`generate current messages = ${JSON.stringify(messages)}`)
const locales = Object.keys(block.messages)
messages = assignLocales(locales, messages)
locales.reduce((messages, locale) => {
if (block.messages[locale]) {
const localeMessages = messages[locale]
const localeBlockMessages = block.messages[locale]
let target: any = localeMessages
const hierarchy = parsed.hierarchy.concat()
while (hierarchy.length >= 0) {
const key = hierarchy.shift()
if (!key) { break }
if (!target[key]) {
target[key] = {}
}
target = target[key]
}
Object.assign(target, localeBlockMessages)
return messages
}
return messages
}, messages)
return messages
}, messages)
}

return messages
}

function writeLocaleMessages (output: string, messages: LocaleMessages) {
// TODO: async implementation
fs.writeFileSync(output, JSON.stringify(messages, null, 2))
Expand Down
98 changes: 41 additions & 57 deletions src/infuser.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,38 @@
import { SFCDescriptor, SFCBlock } from 'vue-template-compiler'
import { Locale, LocaleMessages, SFCFileInfo } from '../types'
import { Locale, MetaLocaleMessage, SFCI18nBlock, SFCFileInfo } from '../types'

import { reflectSFCDescriptor, parseContent, stringfyContent } from './utils'
import { escape, 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[] {
export default function infuse (basePath: string, sources: SFCFileInfo[], meta: MetaLocaleMessage): SFCFileInfo[] {
const descriptors = reflectSFCDescriptor(basePath, sources)
const locales = Object.keys(messages)

return descriptors.map(descriptor => {
return {
content: generate(locales, messages, descriptor),
content: generate(meta, 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)
function generate (meta: MetaLocaleMessage, descriptor: SFCDescriptor): string {
const i18nBlocks = meta.components[descriptor.contentPath]
debug('target i18n blocks\n', i18nBlocks)

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)
const content = buildContent(i18nBlocks, 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) => {
debug(`processing curernt: locale=${locale}, target=${target}`)

const obj = messages[locale]
if (obj) {
let o: any = obj
let prev: any = null
const hierarchy = descriptor.hierarchy.concat()
while (hierarchy.length > 0) {
const key = hierarchy.shift()
debug('processing hierarchy key: ', key)

if (!key || !o) { break }
o = o[key]
prev = o
}

if (!o && !prev) {
return target
} else {
return Object.assign(target, { [locale]: ((!o && prev) ? prev : o) }) as LocaleMessages
}
} else {
return target
}
}, {} as LocaleMessages)
}

function getBlocks (descriptor: SFCDescriptor): SFCBlock[] {
const { template, script, styles, customBlocks } = descriptor
const blocks: SFCBlock[] = [...styles, ...customBlocks]
Expand All @@ -72,36 +42,36 @@ function getBlocks (descriptor: SFCDescriptor): SFCBlock[] {
return blocks
}

function buildContent (target: LocaleMessages, raw: string, blocks: SFCBlock[]): string {
function buildContent (i18nBlocks: SFCI18nBlock[], raw: string, blocks: SFCBlock[]): string {
let offset = 0
let i18nBlockCounter = 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
const locale: Locale | undefined = block.attrs.locale
const i18nBlock = i18nBlocks[i18nBlockCounter]
debug(`meta.lang = ${i18nBlock.lang}, block.lang = ${lang}, meta.locale = ${i18nBlock.locale}, block.locale = ${locale}`)

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)
})
if (lang === i18nBlock.lang && locale === i18nBlock.locale) {
if (locale) {
messages = i18nBlock.messages[locale]
} else {
messages = i18nBlock.messages
}
} else {
messages = Object.assign({}, target[locale])
targetLocales = targetLocales.filter(l => l !== locale)
debug(`unmatch meta block and sfc block`)
messages = parseContent(block.content, lang)
}

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
i18nBlockCounter++
} else {
contents = contents.concat(raw.slice(offset, block.end))
offset = block.end as number
Expand All @@ -110,18 +80,32 @@ function buildContent (target: LocaleMessages, raw: string, blocks: SFCBlock[]):
}, 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>`)
if (i18nBlocks.length > i18nBlockCounter) {
i18nBlocks.slice(i18nBlockCounter).reduce((contents, i18nBlock) => {
contents.push(buildI18nTag(i18nBlock))
return contents
}, contents)
}

return contents.join('')
}

function buildI18nTag (i18nBlock: SFCI18nBlock): string {
const { locale, lang, messages } = i18nBlock
let tag = '<i18n'
if (locale) {
tag += ` locale="${escape(locale)}"`
}
if (lang !== 'json') {
tag += ` lang="${escape(lang)}"`
}
tag += '>'

return `\n
${tag}
${format(stringfyContent(locale ? messages[locale] : messages, lang), lang)}</i18n>`
}

function format (source: string, lang: string): string {
debug(`format: lang=${lang}, source=${source}`)

Expand Down

0 comments on commit 04dee29

Please sign in to comment.