Add CLI args --ftl-path and --ftl-prefix

This commit is contained in:
Eemeli Aro 2021-11-02 20:45:30 +02:00
Родитель a03ec9d81f
Коммит 2c84f03db1
8 изменённых файлов: 118 добавлений и 57 удалений

12
cli.js
Просмотреть файл

@ -25,6 +25,18 @@ yargs(process.argv.slice(2))
default: 'python -m black',
type: 'string'
},
ftlPath: {
alias: 'p',
desc: 'Path to target FTL file',
requiresArg: true,
type: 'string'
},
ftlPrefix: {
alias: 'x',
desc: 'Prefix for Fluent message keys',
requiresArg: true,
type: 'string'
},
root: {
alias: 'r',
desc: 'Root of mozilla-central (usually autodetected)',

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

@ -1,16 +1,24 @@
import { Comment } from '@fluent/syntax'
import { Comment as FluentComment } from '@fluent/syntax'
import {
Comment as PropComment,
EmptyLine as PropEmptyLine
} from 'dot-properties'
import { addFluentPattern } from './add-fluent-pattern.js'
/**
* @param {import('./parse-message-files.js').PropData} propData
* @param {import('./transform-js').TransformOptions} options
*/
export function applyMigration({ ast, ftl, ftlTransform, migrate }) {
export function applyMigration(
{ ast, ftl, ftlTransform, migrate },
{ ftlPath, ftlPrefix }
) {
let commentLines = []
const commentNotes = {}
/**
* @param {string | null} key
* @returns {Comment | null}
* @returns {FluentComment | null}
*/
const getComment = (key) => {
let content = commentLines.join('\n').trim()
@ -25,7 +33,7 @@ export function applyMigration({ ast, ftl, ftlTransform, migrate }) {
const cn = commentNotes[key]
if (cn) content = content ? cn + '\n' + content : cn
}
return key && content ? new Comment(content) : null
return key && content ? new FluentComment(content) : null
}
let nextCutAfter = -1
@ -66,4 +74,17 @@ export function applyMigration({ ast, ftl, ftlTransform, migrate }) {
}
}
}
if (ftlPath || ftlPrefix) {
const insert = []
if (ftlPath) insert.push(new PropComment(`FTL path: ${ftlPath}`))
if (ftlPrefix) insert.push(new PropComment(`FTL prefix: ${ftlPrefix}`))
let pos = 0
while (ast[pos].type === 'COMMENT') pos += 1
if (pos > 0) insert.unshift(new PropEmptyLine())
if (ast[pos].type !== 'EMPTY_LINE') insert.push(new PropEmptyLine())
ast.splice(pos, 0, ...insert)
}
}

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

@ -1,16 +1,15 @@
import { Comment, parse as parseFluent, Resource } from '@fluent/syntax'
import { existsSync } from 'fs'
import { readFile } from 'fs/promises'
import { resolve } from 'path'
import { types, visit } from 'recast'
import { visit } from 'recast'
import { resolveChromeUri } from 'resolve-chrome-uri'
import { parseMessageFiles } from './parse-message-files.js'
import { parseStringBundleTags } from './parse-xhtml.js'
const n = types.namedTypes
export async function findExternalRefs(root, ast) {
/**
* @param {unknown} ast
* @param {import('./transform-js.js').TransformOptions} options
*/
export async function findExternalRefs(ast, options) {
/** @type {import('ast-types').NodePath[]} */
const propertiesUriPaths = []
/** @type {import('ast-types').NodePath[]} */
@ -39,7 +38,7 @@ export async function findExternalRefs(root, ast) {
*/
const xhtml = []
for (const uri of new Set(xhtmlUriPaths.map((path) => path.node.value))) {
const filePaths = await resolveChromeUri(root, uri)
const filePaths = await resolveChromeUri(options.root, uri)
if (filePaths.size === 0) console.warn(`Unresolved URI: ${uri}`)
else {
for (const fp of filePaths) {
@ -56,11 +55,11 @@ export async function findExternalRefs(root, ast) {
/** @type {import('./parse-message-files.js').PropData[]} */
const properties = []
for (const uri of propUris) {
const filePaths = await resolveChromeUri(root, uri)
const filePaths = await resolveChromeUri(options.root, uri)
if (filePaths.size === 0) console.warn(`Unresolved URI: ${uri}`)
else {
for (const fp of filePaths) {
const data = await parseMessageFiles(fp)
const data = await parseMessageFiles(fp, options)
data.uri = uri
properties.push(data)
}

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

@ -22,15 +22,16 @@ import { resolve } from 'path'
/**
* @param {string} path
* @param {import('./transform-js.js').TransformOptions} options
* @returns {Promise<PropData>}
*/
export async function parseMessageFiles(path) {
export async function parseMessageFiles(path, options) {
const src = await readFile(path, 'utf8')
const ast = parseLines(src, true)
const msgKeys = ast
.filter((node) => node.type === 'PAIR')
.map((pair) => pair.key)
const { ftlRoot, ftlPath, ftlPrefix } = getFtlMetadata(path, ast)
const { ftlRoot, ftlPath, ftlPrefix } = getFtlMetadata(path, ast, options)
const ftl = await getFluentResource(ftlRoot, ftlPath)
return {
uri: '', // filled in by caller
@ -55,42 +56,26 @@ export async function parseMessageFiles(path) {
*
* @param {string} propPath - The location of the .properties file
* @param {import('dot-properties').Node[]} ast
* @param {import('./transform-js.js').TransformOptions} options
*/
function getFtlMetadata(propPath, ast) {
let ftlPath = null
let ftlRoot = null
let ftlPrefix = ''
function getFtlMetadata(propPath, ast, options) {
let rawFtlPath = options.ftlPath || null
let ftlPrefix = options.ftlPrefix || ''
for (const node of ast) {
if (node.type === 'COMMENT') {
const match = node.comment.match(/[!#]\s*FTL\s+(path|prefix):(.*)/)
if (match)
switch (match[1]) {
case 'path': {
if (ftlPath)
throw new Error(`FTL path set more than once in ${propPath}`)
const parts = match[2].trim().split('/')
const fi = parts.indexOf('en-US')
if (fi !== -1) {
ftlRoot = parts.splice(0, fi + 1).join('/')
} else {
const propPathParts = propPath.split('/')
const i = propPathParts.indexOf('en-US')
if (i === -1)
throw new Error(
`A full FTL file path is required in ${propPath}`
)
ftlRoot = propPathParts.slice(0, i + 1).join('/')
}
ftlPath = parts.join('/')
if (!ftlPath.endsWith('.ftl'))
throw new Error(
`FTL file path should be fully qualified with an .ftl extension in ${propPath}`
)
if (rawFtlPath)
throw new Error(`FTL path set more than once for ${propPath}`)
rawFtlPath = match[2].trim()
break
}
case 'prefix':
if (ftlPrefix)
throw new Error(`FTL prefix set more than once in ${propPath}`)
throw new Error(`FTL prefix set more than once for ${propPath}`)
ftlPrefix = match[2].trim()
if (/[^a-z-]/.test(ftlPrefix))
throw new Error(
@ -100,9 +85,40 @@ function getFtlMetadata(propPath, ast) {
}
}
}
const { ftlPath, ftlRoot } = parseFtlPath(propPath, rawFtlPath)
return { ftlPath, ftlRoot, ftlPrefix }
}
/**
* @param {string} propPath
* @param {string} raw
*/
function parseFtlPath(propPath, raw) {
/** @type {string} */
let ftlRoot
const parts = raw.split('/')
const fi = parts.indexOf('en-US')
if (fi !== -1) {
ftlRoot = parts.splice(0, fi + 1).join('/')
} else {
const propPathParts = propPath.split('/')
const i = propPathParts.indexOf('en-US')
if (i === -1)
throw new Error(`A full FTL file path is required for ${propPath}`)
ftlRoot = propPathParts.slice(0, i + 1).join('/')
}
const ftlPath = parts.join('/')
if (!ftlPath.endsWith('.ftl'))
throw new Error(
`FTL file path should be fully qualified with an .ftl extension for ${propPath}`
)
return { ftlPath, ftlRoot }
}
const mplLicenseHeader = `
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this

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

@ -18,6 +18,8 @@ const n = types.namedTypes
* bug?: string,
* dryRun?: boolean,
* format?: string,
* ftlPath?: string,
* ftlPrefix?: string,
* root?: string,
* title?: string
* }} TransformOptions
@ -41,22 +43,22 @@ export async function transformJs(jsPath, options = {}) {
// Parse the source JS, XHTML & .properties files
const ast = jsParse(await readFile(jsPath, 'utf8'))
const { properties, xhtml } = await findExternalRefs(options.root, ast)
const { properties, xhtml } = await findExternalRefs(ast, options)
let hasPropMigrations = false
let migrationCount = 0
for (const props of properties) {
if (props.ftl) hasPropMigrations = true
if (props.ftl) migrationCount += 1
else
console.warn(
`Skipping ${relative(options.root, props.path)} (No FTL metadata)`
)
}
if (!hasPropMigrations) {
if (migrationCount === 0) {
console.error(`
Error: No migrations defined!
In order to migrate strings to Fluent, at least one of the .properties files
must include FTL metadata comments:
In order to migrate strings to Fluent, the --ftl-path option must be set or
at least one of the .properties files must include FTL metadata comments:
# FTL path: foo/bar/baz.ftl
# FTL prefix: foobar
@ -65,6 +67,21 @@ For more information, see: https://github.com/eemeli/properties-to-ftl#readme
`)
process.exit(1)
}
if (migrationCount > 1 && (options.ftlPath || options.ftlPrefix)) {
console.error(`
Error: Multiple .properties found together with --ftl-path or --ftl-prefix!
When migrating strings from more than one .properties file, they must include
FTL metadata comments directly, and cannot be given as command line arguments:
# FTL path: foo/bar/baz.ftl
# FTL prefix: foobar
For more information, see: https://github.com/eemeli/properties-to-ftl#readme
`)
process.exit(1)
}
const fixmeNodes = new Set()

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

@ -21,10 +21,7 @@ const execFile = promisify(execFileCb)
* @returns {Promise<boolean>} If `true`, at least one string was migrated.
* If `false`, nothing was written to disk.
*/
export async function updateLocalizationFiles(
propData,
{ bug, dryRun, format, root, title }
) {
export async function updateLocalizationFiles(propData, options) {
if (!propData.ftl) return false
const n = Object.keys(propData.migrate).length
if (n === 0) {
@ -32,12 +29,14 @@ export async function updateLocalizationFiles(
return false
}
applyMigration(propData, options)
const { bug, dryRun, format, root, title } = options
const rpp = relative(root, propData.path)
const fp = resolve(propData.ftlRoot, propData.ftlPath)
let pyPath = ''
let propsRemoved = false
applyMigration(propData)
if (dryRun) {
serializeFluent(propData.ftl)
stringifyProperties(propData.ast, {

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

@ -17,7 +17,7 @@
"scripts": {
"clean": "git clean -fx test/artifacts && git restore test/artifacts",
"test": "npm run test:dlu && npm run test:had",
"test:dlu": "cd test/artifacts && node ../../cli.js toolkit/mozapps/downloads/DownloadUtils.jsm",
"test:dlu": "cd test/artifacts && node ../../cli.js -p downloads.ftl -x downloads toolkit/mozapps/downloads/DownloadUtils.jsm",
"test:had": "cd test/artifacts && node ../../cli.js toolkit/mozapps/downloads/HelperAppDlg.jsm"
},
"dependencies": {

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

@ -2,9 +2,6 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# FTL path: downloads.ftl
# FTL prefix: downloads
# LOCALIZATION NOTE (shortSeconds): Semi-colon list of plural
# forms. See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
# s is the short form for seconds