Add PLURALS transform support
This commit is contained in:
Родитель
34ee912d1b
Коммит
c95f8aa0df
|
@ -5,14 +5,17 @@ import {
|
|||
Message,
|
||||
Pattern,
|
||||
Placeable,
|
||||
SelectExpression,
|
||||
TextElement,
|
||||
VariableReference
|
||||
VariableReference,
|
||||
Variant
|
||||
} from '@fluent/syntax'
|
||||
|
||||
/**
|
||||
* @typedef {{ type: 'COPY', source: string }} CopyTransform
|
||||
* @typedef {{ type: 'REPLACE', source: string, target: string, map: Array<{ from: string, to: string }> }} ReplaceTransform
|
||||
* @typedef { CopyTransform | ReplaceTransform } PatternTransform
|
||||
* @typedef {{ type: 'REPLACE', source: string, map: Array<{ from: string, to: string }> }} ReplaceTransform
|
||||
* @typedef {{ type: 'PLURALS', source: string, selector: string, map: Array<{ from: string, to: string }> }} PluralsTransform
|
||||
* @typedef { CopyTransform | ReplaceTransform | PluralsTransform } PatternTransform
|
||||
*
|
||||
* @typedef {{
|
||||
* id: string,
|
||||
|
@ -34,14 +37,9 @@ import {
|
|||
* @param {Comment | null} comment
|
||||
* @param {import('./migrate-message').MessageMigration} migration
|
||||
*/
|
||||
export function addFluentPattern(
|
||||
ftl,
|
||||
transforms,
|
||||
node,
|
||||
comment,
|
||||
{ key, attr, varNames }
|
||||
) {
|
||||
/** @type {Message} */
|
||||
export function addFluentPattern(ftl, transforms, node, comment, migration) {
|
||||
const { key, attr } = migration
|
||||
/** @type {Message | undefined} */
|
||||
let msg = ftl.body.find(
|
||||
(entry) => entry.type === 'Message' && entry.id.name === key
|
||||
)
|
||||
|
@ -59,16 +57,22 @@ export function addFluentPattern(
|
|||
let cc = comment?.content || ''
|
||||
let vc = 'Variables:'
|
||||
let hasVar = false
|
||||
const pattern = parseMsgPattern(node, varNames, (prev, next) => {
|
||||
hasVar = true
|
||||
let desc = 'FIXME'
|
||||
const re = new RegExp(`${prev.replace('$', '\\$')}([^%\n]+)`)
|
||||
cc = cc.replace(re, (_, prevDesc) => {
|
||||
desc = prevDesc.replace(/^\s*(is\s*)?/, '').replace(/\s*[;,.]?\s*$/, '.')
|
||||
return ''
|
||||
})
|
||||
vc += `\n $${next} (String): ${desc}`
|
||||
})
|
||||
const pattern = parseMessage(
|
||||
node,
|
||||
migration,
|
||||
(/** @type {string} */ prev, /** @type {string} */ next) => {
|
||||
hasVar = true
|
||||
let desc = 'FIXME'
|
||||
const re = new RegExp(`${prev.replace('$', '\\$')}([^%\n]+)`)
|
||||
cc = cc.replace(re, (_, prevDesc) => {
|
||||
desc = prevDesc
|
||||
.replace(/^\s*(is\s*)?/, '')
|
||||
.replace(/\s*[;,.]?\s*$/, '.')
|
||||
return ''
|
||||
})
|
||||
vc += `\n $${next} (String): ${desc}`
|
||||
}
|
||||
)
|
||||
if (hasVar) {
|
||||
const fc = (cc.replace(/\s+\n|\s*$/g, '\n') + vc).trim()
|
||||
if (comment) comment.content = fc
|
||||
|
@ -92,18 +96,69 @@ export function addFluentPattern(
|
|||
|
||||
/**
|
||||
* @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 {Function} fixArgNameInComment
|
||||
*/
|
||||
function parseMsgPattern({ key, value }, varNames, fixArgNameInComment) {
|
||||
function parseMsgPattern(source, varNames, fixArgNameInComment) {
|
||||
/** @type {Array<Placeable|TextElement>} */
|
||||
const elements = []
|
||||
let done = 0
|
||||
let num = 0
|
||||
/** @type {ReplaceTransform['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)
|
||||
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
|
||||
const name = varNames?.[num - 1] || `var${num}`
|
||||
const id = new Identifier(name)
|
||||
|
@ -112,13 +167,7 @@ function parseMsgPattern({ key, value }, varNames, fixArgNameInComment) {
|
|||
done = match.index + match[0].length
|
||||
fixArgNameInComment(match[0], name)
|
||||
}
|
||||
if (done < value.length) elements.push(new TextElement(value.substring(done)))
|
||||
|
||||
/** @type {PatternTransform} */
|
||||
const transform =
|
||||
Object.keys(map).length === 0
|
||||
? { type: 'COPY', source: key }
|
||||
: { type: 'REPLACE', source: key, map }
|
||||
|
||||
return { ftl: new Pattern(elements), transform }
|
||||
if (done < source.length)
|
||||
elements.push(new TextElement(source.substring(done)))
|
||||
return { ftl: new Pattern(elements), map }
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
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}
|
||||
*/
|
||||
export function migrateMessage(propData, propKey, varNames) {
|
||||
if (!varNames) varNames = initVarNames(propData, propKey)
|
||||
const { migrate } = propData
|
||||
const { ast, 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]
|
||||
if (prev) {
|
||||
for (let i = prev.varNames.length; i < varNames.length; ++i) {
|
||||
|
@ -49,16 +58,15 @@ export function migrateMessage(propData, propKey, varNames) {
|
|||
return {
|
||||
key: getFtlKey(propData, key),
|
||||
attr: attr ? kebabCase(attr) : null,
|
||||
plural: propNode?.value?.includes(';') ? 'FIXME' : null,
|
||||
varNames
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./parse-message-files.js').PropData} propData
|
||||
* @param {string} propKey
|
||||
* @param {import('dot-properties').Pair | undefined} node
|
||||
*/
|
||||
function initVarNames({ ast }, propKey) {
|
||||
const node = ast.find((node) => node.type === 'PAIR' && node.key === propKey)
|
||||
function initVarNames(node) {
|
||||
/** @type {string[]} */
|
||||
const varNames = []
|
||||
if (node) {
|
||||
|
@ -78,7 +86,7 @@ function initVarNames({ ast }, propKey) {
|
|||
|
||||
/**
|
||||
* @param {import('./parse-message-files.js').PropData} propData
|
||||
* @param {string} propKey
|
||||
* @param {string} key
|
||||
* @returns {string}
|
||||
*/
|
||||
function getFtlKey({ migrate, ftlPrefix, ftl }, key) {
|
||||
|
|
|
@ -12,9 +12,11 @@ export const configPath = (propPath) =>
|
|||
propPath.replace(/properties$/, 'migration.yaml')
|
||||
|
||||
/**
|
||||
* @param {string} root
|
||||
* @param {string} propPath - Path to `.properties` file
|
||||
* @returns {Promise<{
|
||||
* ftl: { root: string, path: string },
|
||||
* meta: { bug: string, title: string },
|
||||
* migrate: Record<string, import('./migrate-message.js').MessageMigration>
|
||||
* } | null>}
|
||||
*/
|
||||
|
@ -30,6 +32,13 @@ export async function readMigrationConfig(root, propPath) {
|
|||
const relPath = relative(root, cfgPath)
|
||||
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' })
|
||||
if (
|
||||
!meta ||
|
||||
|
@ -73,9 +82,20 @@ export async function readMigrationConfig(root, propPath) {
|
|||
!Array.isArray(value.varNames)
|
||||
) {
|
||||
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('./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 }) {
|
||||
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'
|
||||
for (const pair of doc.contents.items) pair.key.spaceBefore = true
|
||||
|
||||
/** @type {string[]} */
|
||||
const fixVars = []
|
||||
doc.get('migrate').items = msgKeys.map((propKey) => {
|
||||
/** @type {import('yaml').Pair} */
|
||||
let pair
|
||||
/** @type {import('./migrate-message.js').MessageMigration | null} */
|
||||
let mm = null
|
||||
if (migrate.hasOwnProperty(propKey)) {
|
||||
mm = migrate[propKey]
|
||||
|
@ -131,6 +154,10 @@ export async function writeMigrationConfig(propData, { all, bug, root }) {
|
|||
const mc = prettyMessageMigration(migrateMessage(propData, propKey, null))
|
||||
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) {
|
||||
for (let i = 0; i < mm.varNames.length; ++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
|
||||
*/
|
||||
function prettyMessageMigration({ key, attr, varNames }) {
|
||||
function prettyMessageMigration({ key, attr, plural, varNames }) {
|
||||
/** @type {Partial<import('./migrate-message.js').MessageMigration>} */
|
||||
const value = { key }
|
||||
if (attr) value.attr = attr
|
||||
if (plural) value.plural = null
|
||||
if (varNames.length > 0) value.varNames = varNames
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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', '')
|
||||
|
||||
/**
|
||||
|
@ -57,7 +57,7 @@ def migrate(ctx):
|
|||
|
||||
/**
|
||||
* @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) {
|
||||
const key = JSON.stringify(pt.source)
|
||||
|
@ -74,6 +74,22 @@ function compilePattern(pt, imports) {
|
|||
)
|
||||
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}`)
|
||||
}
|
||||
|
|
|
@ -214,6 +214,8 @@ For more information, see: https://github.com/mozilla/properties-to-ftl#readme
|
|||
if (keySrc !== key && ftlMsg.attr) {
|
||||
ftlKey += '.' + ftlMsg.attr
|
||||
fixmeNodes.add(keySrc)
|
||||
} else if (ftlMsg.plural) {
|
||||
fixmeNodes.add(keySrc)
|
||||
}
|
||||
if (propData.hasMigrateConfig) keySrc.value = ftlKey
|
||||
msgKeyLiterals.set(keySrc, ftlMsg)
|
||||
|
|
Загрузка…
Ссылка в новой задаче