This commit is contained in:
Eemeli Aro 2021-11-30 16:41:40 +02:00
Родитель 34ee912d1b
Коммит c95f8aa0df
5 изменённых файлов: 150 добавлений и 46 удалений

Просмотреть файл

@ -5,14 +5,17 @@ import {
Message, Message,
Pattern, Pattern,
Placeable, Placeable,
SelectExpression,
TextElement, TextElement,
VariableReference VariableReference,
Variant
} from '@fluent/syntax' } from '@fluent/syntax'
/** /**
* @typedef {{ type: 'COPY', source: string }} CopyTransform * @typedef {{ type: 'COPY', source: string }} CopyTransform
* @typedef {{ type: 'REPLACE', source: string, target: string, map: Array<{ from: string, to: string }> }} ReplaceTransform * @typedef {{ type: 'REPLACE', source: string, map: Array<{ from: string, to: string }> }} ReplaceTransform
* @typedef { CopyTransform | ReplaceTransform } PatternTransform * @typedef {{ type: 'PLURALS', source: string, selector: string, map: Array<{ from: string, to: string }> }} PluralsTransform
* @typedef { CopyTransform | ReplaceTransform | PluralsTransform } PatternTransform
* *
* @typedef {{ * @typedef {{
* id: string, * id: string,
@ -34,14 +37,9 @@ import {
* @param {Comment | null} comment * @param {Comment | null} comment
* @param {import('./migrate-message').MessageMigration} migration * @param {import('./migrate-message').MessageMigration} migration
*/ */
export function addFluentPattern( export function addFluentPattern(ftl, transforms, node, comment, migration) {
ftl, const { key, attr } = migration
transforms, /** @type {Message | undefined} */
node,
comment,
{ key, attr, varNames }
) {
/** @type {Message} */
let msg = ftl.body.find( let msg = ftl.body.find(
(entry) => entry.type === 'Message' && entry.id.name === key (entry) => entry.type === 'Message' && entry.id.name === key
) )
@ -59,16 +57,22 @@ export function addFluentPattern(
let cc = comment?.content || '' let cc = comment?.content || ''
let vc = 'Variables:' let vc = 'Variables:'
let hasVar = false let hasVar = false
const pattern = parseMsgPattern(node, varNames, (prev, next) => { const pattern = parseMessage(
hasVar = true node,
let desc = 'FIXME' migration,
const re = new RegExp(`${prev.replace('$', '\\$')}([^%\n]+)`) (/** @type {string} */ prev, /** @type {string} */ next) => {
cc = cc.replace(re, (_, prevDesc) => { hasVar = true
desc = prevDesc.replace(/^\s*(is\s*)?/, '').replace(/\s*[;,.]?\s*$/, '.') let desc = 'FIXME'
return '' const re = new RegExp(`${prev.replace('$', '\\$')}([^%\n]+)`)
}) cc = cc.replace(re, (_, prevDesc) => {
vc += `\n $${next} (String): ${desc}` desc = prevDesc
}) .replace(/^\s*(is\s*)?/, '')
.replace(/\s*[;,.]?\s*$/, '.')
return ''
})
vc += `\n $${next} (String): ${desc}`
}
)
if (hasVar) { if (hasVar) {
const fc = (cc.replace(/\s+\n|\s*$/g, '\n') + vc).trim() const fc = (cc.replace(/\s+\n|\s*$/g, '\n') + vc).trim()
if (comment) comment.content = fc if (comment) comment.content = fc
@ -92,18 +96,69 @@ export function addFluentPattern(
/** /**
* @param {import('dot-properties').Pair} node * @param {import('dot-properties').Pair} node
* @param {import('./migrate-message').MessageMigration} migration
* @param {Function} fixArgNameInComment
* @returns {{ ftl: Pattern, transform: PatternTransform }}
*/
function parseMessage(
{ key, value },
{ plural, varNames },
fixArgNameInComment
) {
if (plural) {
const sep = value.indexOf(';')
const caseOne = value.substring(0, sep)
const caseOther = value.substring(sep + 1)
/** @type {Variant[]} */
const variants = []
/** @type {ReplaceTransform['map'] | null} */
let map = null
if (caseOne) {
const one = parseMsgPattern(caseOne, varNames, fixArgNameInComment)
variants.push(new Variant(new Identifier('one'), one.ftl, false))
map = one.map
}
const other = parseMsgPattern(caseOther, varNames, fixArgNameInComment)
variants.push(new Variant(new Identifier('other'), other.ftl, true))
map = map ? map.concat(other.map) : other.map
const selector = new VariableReference(new Identifier(plural))
const selExp = new SelectExpression(selector, variants)
return {
ftl: new Pattern([new Placeable(selExp)]),
transform: { type: 'PLURALS', source: key, selector: plural, map }
}
} else {
const { ftl, map } = parseMsgPattern(value, varNames, fixArgNameInComment)
/** @type {PatternTransform} */
const transform =
Object.keys(map).length === 0
? { type: 'COPY', source: key }
: { type: 'REPLACE', source: key, map }
return { ftl, transform }
}
}
/**
* @param {string} source
* @param {string[]} varNames * @param {string[]} varNames
* @param {Function} fixArgNameInComment * @param {Function} fixArgNameInComment
*/ */
function parseMsgPattern({ key, value }, varNames, fixArgNameInComment) { function parseMsgPattern(source, varNames, fixArgNameInComment) {
/** @type {Array<Placeable|TextElement>} */
const elements = [] const elements = []
let done = 0 let done = 0
let num = 0 let num = 0
/** @type {ReplaceTransform['map']} */ /** @type {ReplaceTransform['map']} */
const map = [] const map = []
for (const match of value.matchAll(/%(\d\$)?S/g)) { for (const match of source.matchAll(/%(\d\$)?S/g)) {
if (match.index > done) if (match.index > done)
elements.push(new TextElement(value.substring(done, match.index))) elements.push(new TextElement(source.substring(done, match.index)))
num = match[1] ? parseInt(match[1]) : num + 1 num = match[1] ? parseInt(match[1]) : num + 1
const name = varNames?.[num - 1] || `var${num}` const name = varNames?.[num - 1] || `var${num}`
const id = new Identifier(name) const id = new Identifier(name)
@ -112,13 +167,7 @@ function parseMsgPattern({ key, value }, varNames, fixArgNameInComment) {
done = match.index + match[0].length done = match.index + match[0].length
fixArgNameInComment(match[0], name) fixArgNameInComment(match[0], name)
} }
if (done < value.length) elements.push(new TextElement(value.substring(done))) if (done < source.length)
elements.push(new TextElement(source.substring(done)))
/** @type {PatternTransform} */ return { ftl: new Pattern(elements), map }
const transform =
Object.keys(map).length === 0
? { type: 'COPY', source: key }
: { type: 'REPLACE', source: key, map }
return { ftl: new Pattern(elements), transform }
} }

Просмотреть файл

@ -1,7 +1,12 @@
import kebabCase from 'lodash.kebabcase' import kebabCase from 'lodash.kebabcase'
/** /**
* @typedef {{ key: string, attr: string | null, varNames: string[] }} MessageMigration * @typedef {{
* key: string,
* attr: string | null,
* plural: string | false | null,
* varNames: string[]
* }} MessageMigration
*/ */
/** /**
@ -15,8 +20,12 @@ import kebabCase from 'lodash.kebabcase'
* @returns {MessageMigration} * @returns {MessageMigration}
*/ */
export function migrateMessage(propData, propKey, varNames) { export function migrateMessage(propData, propKey, varNames) {
if (!varNames) varNames = initVarNames(propData, propKey) const { ast, migrate } = propData
const { migrate } = propData /** @type {import('dot-properties').Pair | undefined} */
const propNode = ast.find(
(node) => node.type === 'PAIR' && node.key === propKey
)
if (!varNames) varNames = initVarNames(propNode)
const prev = migrate[propKey] const prev = migrate[propKey]
if (prev) { if (prev) {
for (let i = prev.varNames.length; i < varNames.length; ++i) { for (let i = prev.varNames.length; i < varNames.length; ++i) {
@ -49,16 +58,15 @@ export function migrateMessage(propData, propKey, varNames) {
return { return {
key: getFtlKey(propData, key), key: getFtlKey(propData, key),
attr: attr ? kebabCase(attr) : null, attr: attr ? kebabCase(attr) : null,
plural: propNode?.value?.includes(';') ? 'FIXME' : null,
varNames varNames
} }
} }
/** /**
* @param {import('./parse-message-files.js').PropData} propData * @param {import('dot-properties').Pair | undefined} node
* @param {string} propKey
*/ */
function initVarNames({ ast }, propKey) { function initVarNames(node) {
const node = ast.find((node) => node.type === 'PAIR' && node.key === propKey)
/** @type {string[]} */ /** @type {string[]} */
const varNames = [] const varNames = []
if (node) { if (node) {
@ -78,7 +86,7 @@ function initVarNames({ ast }, propKey) {
/** /**
* @param {import('./parse-message-files.js').PropData} propData * @param {import('./parse-message-files.js').PropData} propData
* @param {string} propKey * @param {string} key
* @returns {string} * @returns {string}
*/ */
function getFtlKey({ migrate, ftlPrefix, ftl }, key) { function getFtlKey({ migrate, ftlPrefix, ftl }, key) {

Просмотреть файл

@ -12,9 +12,11 @@ export const configPath = (propPath) =>
propPath.replace(/properties$/, 'migration.yaml') propPath.replace(/properties$/, 'migration.yaml')
/** /**
* @param {string} root
* @param {string} propPath - Path to `.properties` file * @param {string} propPath - Path to `.properties` file
* @returns {Promise<{ * @returns {Promise<{
* ftl: { root: string, path: string }, * ftl: { root: string, path: string },
* meta: { bug: string, title: string },
* migrate: Record<string, import('./migrate-message.js').MessageMigration> * migrate: Record<string, import('./migrate-message.js').MessageMigration>
* } | null>} * } | null>}
*/ */
@ -30,6 +32,13 @@ export async function readMigrationConfig(root, propPath) {
const relPath = relative(root, cfgPath) const relPath = relative(root, cfgPath)
console.warn(`Found migration config at ${relPath}`) console.warn(`Found migration config at ${relPath}`)
/**
* @type {{
* ftl?: { root: string, path: string },
* meta?: { bug: string, title: string },
* migrate: Record<string, import('./migrate-message.js').MessageMigration>
* }}
*/
const { ftl, meta, migrate } = parse(src, { schema: 'failsafe' }) const { ftl, meta, migrate } = parse(src, { schema: 'failsafe' })
if ( if (
!meta || !meta ||
@ -73,9 +82,20 @@ export async function readMigrationConfig(root, propPath) {
!Array.isArray(value.varNames) !Array.isArray(value.varNames)
) { ) {
throw new Error( throw new Error(
`Invalid migrate.${key} value in ${relPath}, expected { key: string, attr?: string, varNames?: string[] } or null` `Invalid migrate.${key} value in ${relPath}, expected { key: string, attr?: string, plural?: string | false, varNames?: string[] } or null`
) )
} }
if ('plural' in value) {
if (
value.plural
? typeof value.plural !== 'string'
: value.plural !== false
) {
throw new Error(
`Invalid migrate.${key}.plural value in ${relPath}, expected non-empty string or false`
)
}
} else value.plural = null
} }
} }
@ -85,7 +105,7 @@ export async function readMigrationConfig(root, propPath) {
/** /**
* @param {import('./parse-message-files.js').PropData} propData * @param {import('./parse-message-files.js').PropData} propData
* @param {import('./transform-js.js').TransformOptions} options * @param {import('./transform-js.js').TransformOptions} options
* @returns {string} Path to migration config file * @returns {Promise<string>} Path to migration config file
*/ */
export async function writeMigrationConfig(propData, { all, bug, root }) { export async function writeMigrationConfig(propData, { all, bug, root }) {
const { ast, ftlPath, ftlRoot, migrate, msgKeys, path } = propData const { ast, ftlPath, ftlRoot, migrate, msgKeys, path } = propData
@ -116,9 +136,12 @@ export async function writeMigrationConfig(propData, { all, bug, root }) {
if (!bug) doc.getIn(['meta', 'bug'], true).comment = ' FIXME' if (!bug) doc.getIn(['meta', 'bug'], true).comment = ' FIXME'
for (const pair of doc.contents.items) pair.key.spaceBefore = true for (const pair of doc.contents.items) pair.key.spaceBefore = true
/** @type {string[]} */
const fixVars = [] const fixVars = []
doc.get('migrate').items = msgKeys.map((propKey) => { doc.get('migrate').items = msgKeys.map((propKey) => {
/** @type {import('yaml').Pair} */
let pair let pair
/** @type {import('./migrate-message.js').MessageMigration | null} */
let mm = null let mm = null
if (migrate.hasOwnProperty(propKey)) { if (migrate.hasOwnProperty(propKey)) {
mm = migrate[propKey] mm = migrate[propKey]
@ -131,6 +154,10 @@ export async function writeMigrationConfig(propData, { all, bug, root }) {
const mc = prettyMessageMigration(migrateMessage(propData, propKey, null)) const mc = prettyMessageMigration(migrateMessage(propData, propKey, null))
pair.value.commentBefore = stringify(mc).replace(/^/gm, ' ').trimEnd() pair.value.commentBefore = stringify(mc).replace(/^/gm, ' ').trimEnd()
} }
if (mm?.plural) {
const node = pair.value.get('plural', true)
if (node && !node.value) node.comment = ' FIXME'
}
if (mm?.varNames) { if (mm?.varNames) {
for (let i = 0; i < mm.varNames.length; ++i) { for (let i = 0; i < mm.varNames.length; ++i) {
if (/^var\d+$/.test(mm.varNames[i])) { if (/^var\d+$/.test(mm.varNames[i])) {
@ -176,9 +203,11 @@ it should NOT be added to the source repo.
/** /**
* @param {import('./migrate-message').MessageMigration} migration * @param {import('./migrate-message').MessageMigration} migration
*/ */
function prettyMessageMigration({ key, attr, varNames }) { function prettyMessageMigration({ key, attr, plural, varNames }) {
/** @type {Partial<import('./migrate-message.js').MessageMigration>} */
const value = { key } const value = { key }
if (attr) value.attr = attr if (attr) value.attr = attr
if (plural) value.plural = null
if (varNames.length > 0) value.varNames = varNames if (varNames.length > 0) value.varNames = varNames
return value return value
} }

Просмотреть файл

@ -1,6 +1,6 @@
import { relative, resolve } from 'path' import { relative, resolve } from 'path'
const l10nPath = (root, path) => const l10nPath = (/** @type {string} */ root, /** @type {string} */ path) =>
(path[0] === '/' ? relative(root, path) : path).replace('/locales/en-US', '') (path[0] === '/' ? relative(root, path) : path).replace('/locales/en-US', '')
/** /**
@ -57,7 +57,7 @@ def migrate(ctx):
/** /**
* @param {import('./add-fluent-pattern.js').PatternTransform} pt * @param {import('./add-fluent-pattern.js').PatternTransform} pt
* @param {{ helpers: Set<string>, transforms: Set<string>} imports * @param {{ helpers: Set<string>, transforms: Set<string>}} imports
*/ */
function compilePattern(pt, imports) { function compilePattern(pt, imports) {
const key = JSON.stringify(pt.source) const key = JSON.stringify(pt.source)
@ -74,6 +74,22 @@ function compilePattern(pt, imports) {
) )
return `REPLACE(source, ${key}, { ${replace.join(', ')} })` return `REPLACE(source, ${key}, { ${replace.join(', ')} })`
} }
case 'PLURALS': {
imports.transforms.add('PLURALS')
imports.helpers.add('VARIABLE_REFERENCE')
const args = [`VARIABLE_REFERENCE(${JSON.stringify(pt.selector)})`]
if (pt.map.length > 0) {
imports.transforms.add('REPLACE_IN_TEXT')
const replace = pt.map.map(
({ from, to }) =>
`${JSON.stringify(from)}: VARIABLE_REFERENCE(${JSON.stringify(to)})`
)
args.push(
`lambda text: REPLACE_IN_TEXT(text, { ${replace.join(', ')} })`
)
}
return `PLURALS(source, ${key}, ${args.join(', ')})`
}
} }
throw new Error(`Unknown pattern transform ${pt.type}`) throw new Error(`Unknown pattern transform ${pt.type}`)
} }

Просмотреть файл

@ -214,6 +214,8 @@ For more information, see: https://github.com/mozilla/properties-to-ftl#readme
if (keySrc !== key && ftlMsg.attr) { if (keySrc !== key && ftlMsg.attr) {
ftlKey += '.' + ftlMsg.attr ftlKey += '.' + ftlMsg.attr
fixmeNodes.add(keySrc) fixmeNodes.add(keySrc)
} else if (ftlMsg.plural) {
fixmeNodes.add(keySrc)
} }
if (propData.hasMigrateConfig) keySrc.value = ftlKey if (propData.hasMigrateConfig) keySrc.value = ftlKey
msgKeyLiterals.set(keySrc, ftlMsg) msgKeyLiterals.set(keySrc, ftlMsg)