feat(SW-706): add Lokalise tooling and codemod
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable formatjs/enforce-description */
|
||||
/* eslint-disable formatjs/enforce-default-message */
|
||||
/* eslint-disable formatjs/no-id */
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
export default async function ServerComponentWithIntl() {
|
||||
const intl = await getIntl()
|
||||
|
||||
return <h1>{intl.formatMessage({
|
||||
defaultMessage: "some value goes here",
|
||||
description: {}
|
||||
})}</h1>
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable formatjs/enforce-description */
|
||||
/* eslint-disable formatjs/enforce-default-message */
|
||||
/* eslint-disable formatjs/no-id */
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
export default async function ServerComponentWithTernaryInside() {
|
||||
const intl = await getIntl()
|
||||
|
||||
const someId = 'someId'
|
||||
const data = {
|
||||
type: 'something'
|
||||
}
|
||||
|
||||
return (
|
||||
<h1>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: someId,
|
||||
description: {}
|
||||
})}
|
||||
{intl.formatMessage({
|
||||
defaultMessage: data.type,
|
||||
description: {}
|
||||
})}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable formatjs/enforce-description */
|
||||
/* eslint-disable formatjs/enforce-default-message */
|
||||
/* eslint-disable formatjs/no-id */
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
export default async function ServerComponentWithTernaryInside() {
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<h1>
|
||||
{true ? intl.formatMessage({
|
||||
defaultMessage: "Some true string",
|
||||
description: {}
|
||||
}) : intl.formatMessage({
|
||||
defaultMessage: "Some false string",
|
||||
description: {}
|
||||
})}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable formatjs/enforce-description */
|
||||
/* eslint-disable formatjs/enforce-default-message */
|
||||
/* eslint-disable formatjs/no-id */
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
export default async function ServerComponentWithTernaryInside() {
|
||||
const intl = await getIntl()
|
||||
const variable = "some value"
|
||||
|
||||
return (
|
||||
<h1>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: `String with {variable}`,
|
||||
description: {}
|
||||
}, { variable: variable })}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable formatjs/enforce-description */
|
||||
/* eslint-disable formatjs/enforce-default-message */
|
||||
/* eslint-disable formatjs/no-id */
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
export default async function ServerComponentWithIntl() {
|
||||
const intl = await getIntl()
|
||||
|
||||
return <h1>{intl.formatMessage({ id: "some value goes here" })}</h1>
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable formatjs/enforce-description */
|
||||
/* eslint-disable formatjs/enforce-default-message */
|
||||
/* eslint-disable formatjs/no-id */
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
export default async function ServerComponentWithTernaryInside() {
|
||||
const intl = await getIntl()
|
||||
|
||||
const someId = 'someId'
|
||||
const data = {
|
||||
type: 'something'
|
||||
}
|
||||
|
||||
return (
|
||||
<h1>
|
||||
{intl.formatMessage({ id: someId })}
|
||||
{intl.formatMessage({ id: data.type })}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable formatjs/enforce-description */
|
||||
/* eslint-disable formatjs/enforce-default-message */
|
||||
/* eslint-disable formatjs/no-id */
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
export default async function ServerComponentWithTernaryInside() {
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<h1>
|
||||
{intl.formatMessage(
|
||||
true ? { id: "Some true string" } : { id: "Some false string" }
|
||||
)}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* eslint-disable formatjs/enforce-description */
|
||||
/* eslint-disable formatjs/enforce-default-message */
|
||||
/* eslint-disable formatjs/no-id */
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
export default async function ServerComponentWithTernaryInside() {
|
||||
const intl = await getIntl()
|
||||
const variable = "some value"
|
||||
|
||||
return (
|
||||
<h1>
|
||||
{intl.formatMessage({ id: `String with ${variable}` })}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
13
apps/scandic-web/codemods/lokalise/index.mjs
Normal file
13
apps/scandic-web/codemods/lokalise/index.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Run the lokalise.ts through Jiti
|
||||
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import createJiti from "jiti"
|
||||
|
||||
const lokalise = createJiti(fileURLToPath(import.meta.url))("./lokalise.ts")
|
||||
|
||||
lokalise.codemod([
|
||||
"**/*.{ts,tsx}",
|
||||
"!**/codemods/lokalise/**", // ignore itself
|
||||
"!**/node_modules/**", // ignore node_modules
|
||||
])
|
||||
134
apps/scandic-web/codemods/lokalise/lokalise.test.ts
Normal file
134
apps/scandic-web/codemods/lokalise/lokalise.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
import { describe, expect, test } from "@jest/globals"
|
||||
import { IndentationText, Project } from "ts-morph"
|
||||
|
||||
import { processSourceFile } from "./lokalise"
|
||||
|
||||
describe("Lokalise codemod", () => {
|
||||
test("serverComponent: intl.formatMessage with only id", () => {
|
||||
const input = readFileSync(
|
||||
join(__dirname, "./fixtures/serverComponentOnlyId.tsx"),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
)
|
||||
|
||||
const expected = readFileSync(
|
||||
join(__dirname, "./expectations/serverComponentOnlyId.tsx"),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
)
|
||||
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
},
|
||||
})
|
||||
const sourceFile = project.createSourceFile(
|
||||
"./fixtures/serverComponent.tsx",
|
||||
input
|
||||
)
|
||||
|
||||
processSourceFile(sourceFile)
|
||||
|
||||
const result = sourceFile.getFullText()
|
||||
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
test("serverComponent: intl.formatMessage with ternary inside", () => {
|
||||
const input = readFileSync(
|
||||
join(__dirname, "./fixtures/serverComponentWithTernaryInside.tsx"),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
)
|
||||
|
||||
const expected = readFileSync(
|
||||
join(__dirname, "./expectations/serverComponentWithTernaryInside.tsx"),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
)
|
||||
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
},
|
||||
})
|
||||
const sourceFile = project.createSourceFile(
|
||||
"./fixtures/serverComponentWithTernaryInside.tsx",
|
||||
input
|
||||
)
|
||||
|
||||
processSourceFile(sourceFile)
|
||||
|
||||
const result = sourceFile.getFullText()
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
test("serverComponent: intl.formatMessage with variables", () => {
|
||||
const input = readFileSync(
|
||||
join(__dirname, "./fixtures/serverComponentWithVariables.tsx"),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
)
|
||||
|
||||
const expected = readFileSync(
|
||||
join(__dirname, "./expectations/serverComponentWithVariables.tsx"),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
)
|
||||
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
},
|
||||
})
|
||||
const sourceFile = project.createSourceFile(
|
||||
"./fixtures/serverComponentWithVariables.tsx",
|
||||
input
|
||||
)
|
||||
|
||||
processSourceFile(sourceFile)
|
||||
|
||||
const result = sourceFile.getFullText()
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
test("serverComponent: intl.formatMessage with variable for id", () => {
|
||||
const input = readFileSync(
|
||||
join(__dirname, "./fixtures/serverComponentVariableId.tsx"),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
)
|
||||
|
||||
const expected = readFileSync(
|
||||
join(__dirname, "./expectations/serverComponentVariableId.tsx"),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
)
|
||||
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
},
|
||||
})
|
||||
const sourceFile = project.createSourceFile(
|
||||
"./fixtures/serverComponentVariableId.tsx",
|
||||
input
|
||||
)
|
||||
|
||||
processSourceFile(sourceFile)
|
||||
|
||||
const result = sourceFile.getFullText()
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
||||
228
apps/scandic-web/codemods/lokalise/lokalise.ts
Normal file
228
apps/scandic-web/codemods/lokalise/lokalise.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import {
|
||||
type Expression,
|
||||
type Node,
|
||||
type ObjectLiteralExpression,
|
||||
Project,
|
||||
type SourceFile,
|
||||
ts,
|
||||
} from "ts-morph"
|
||||
|
||||
export function codemod(paths: string) {
|
||||
const project = new Project()
|
||||
project.addSourceFilesAtPaths(paths)
|
||||
|
||||
project.getSourceFiles().forEach((file) => {
|
||||
processSourceFile(file)
|
||||
file.saveSync()
|
||||
})
|
||||
}
|
||||
|
||||
export function processSourceFile(file: SourceFile): void {
|
||||
file.getDescendantsOfKind(ts.SyntaxKind.CallExpression).forEach((call) => {
|
||||
const expression = call.getExpression().getText()
|
||||
|
||||
if (expression === "intl.formatMessage") {
|
||||
const formatMessageArgs = call.getArguments()
|
||||
|
||||
if (formatMessageArgs.length > 0) {
|
||||
const firstFormatMessageArg = formatMessageArgs[0]
|
||||
|
||||
// Handle object literal in the first argument
|
||||
if (
|
||||
firstFormatMessageArg.getKind() ===
|
||||
ts.SyntaxKind.ObjectLiteralExpression
|
||||
) {
|
||||
const expressionVariables = getVariableArguments(
|
||||
firstFormatMessageArg.asKindOrThrow(
|
||||
ts.SyntaxKind.ObjectLiteralExpression
|
||||
)
|
||||
)
|
||||
if (expressionVariables === "{}") {
|
||||
// No variables
|
||||
transformObjectLiteral(
|
||||
firstFormatMessageArg.asKindOrThrow(
|
||||
ts.SyntaxKind.ObjectLiteralExpression
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Handle variables
|
||||
const expressionReplacement = `intl.formatMessage(${transformObjectLiteralAndReturn(
|
||||
firstFormatMessageArg.asKindOrThrow(
|
||||
ts.SyntaxKind.ObjectLiteralExpression
|
||||
)
|
||||
)}, ${expressionVariables})`
|
||||
|
||||
call.replaceWithText(expressionReplacement)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ternary expressions in the first argument
|
||||
else if (
|
||||
firstFormatMessageArg.getKind() ===
|
||||
ts.SyntaxKind.ConditionalExpression
|
||||
) {
|
||||
const conditional = firstFormatMessageArg.asKindOrThrow(
|
||||
ts.SyntaxKind.ConditionalExpression
|
||||
)
|
||||
|
||||
const whenTrue = conditional.getWhenTrue()
|
||||
const whenFalse = conditional.getWhenFalse()
|
||||
|
||||
// Check for variables in message
|
||||
const varArgsWhenTrue = getVariableArguments(whenTrue)
|
||||
const varArgsWhenFalse = getVariableArguments(whenFalse)
|
||||
|
||||
// Replacements
|
||||
const whenTrueReplacement =
|
||||
varArgsWhenTrue !== "{}"
|
||||
? `intl.formatMessage(${transformObjectLiteralAndReturn(whenTrue)}, ${varArgsWhenTrue})`
|
||||
: `intl.formatMessage(${transformObjectLiteralAndReturn(whenTrue)})`
|
||||
|
||||
const whenFalseReplacement =
|
||||
varArgsWhenFalse !== "{}"
|
||||
? `intl.formatMessage(${transformObjectLiteralAndReturn(whenFalse)}, ${varArgsWhenFalse})`
|
||||
: `intl.formatMessage(${transformObjectLiteralAndReturn(whenFalse)})`
|
||||
|
||||
// Replace the ternary expression
|
||||
call.replaceWithText(
|
||||
`${conditional.getCondition().getText()} ? ${whenTrueReplacement} : ${whenFalseReplacement}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Format the file using its existing formatting rules
|
||||
file.formatText({
|
||||
indentSize: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to transform object literals
|
||||
function transformObjectLiteral(objectLiteral: ObjectLiteralExpression): void {
|
||||
const idProperty = objectLiteral
|
||||
.getProperties()
|
||||
.find((prop) => {
|
||||
const p = prop.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
|
||||
return p.getName() === "id"
|
||||
})
|
||||
?.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
|
||||
|
||||
if (idProperty) {
|
||||
const idValue = idProperty.getInitializer()?.getText()
|
||||
|
||||
if (idValue) {
|
||||
// Add defaultMessage
|
||||
if (
|
||||
!objectLiteral
|
||||
.getProperties()
|
||||
.some(
|
||||
(prop) => "getName" in prop && prop.getName() === "defaultMessage"
|
||||
)
|
||||
) {
|
||||
objectLiteral.addPropertyAssignment({
|
||||
name: "defaultMessage",
|
||||
initializer: idValue,
|
||||
})
|
||||
}
|
||||
|
||||
// Remove the id property
|
||||
idProperty.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// Add description if not present
|
||||
// if (
|
||||
// !objectLiteral
|
||||
// .getProperties()
|
||||
// .some((prop) => "getName" in prop && prop.getName() === "description")
|
||||
// ) {
|
||||
// objectLiteral.addPropertyAssignment({
|
||||
// name: "description",
|
||||
// initializer: `{}`,
|
||||
// })
|
||||
// }
|
||||
|
||||
// Extract variables from the defaultMessage if present
|
||||
const defaultMessageProp = objectLiteral
|
||||
.getProperties()
|
||||
.find((prop) => "getName" in prop && prop.getName() === "defaultMessage")
|
||||
?.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
|
||||
|
||||
if (defaultMessageProp) {
|
||||
const defaultMessageValue = defaultMessageProp.getInitializer()?.getText()
|
||||
|
||||
if (defaultMessageValue) {
|
||||
const extractedVariables =
|
||||
extractVariablesFromTemplateString(defaultMessageValue)
|
||||
if (extractedVariables.length > 0) {
|
||||
// Replace the variables in the defaultMessage with FormatJS placeholders
|
||||
const transformedMessage = replaceWithFormatJSPlaceholders(
|
||||
defaultMessageValue,
|
||||
extractedVariables
|
||||
)
|
||||
defaultMessageProp.setInitializer(`${transformedMessage}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract variables from a template string
|
||||
function extractVariablesFromTemplateString(templateString: string): string[] {
|
||||
const regex = /\${(.*?)}/g
|
||||
const variables: string[] = []
|
||||
let match
|
||||
|
||||
// Find all variable references in the template literal
|
||||
while ((match = regex.exec(templateString)) !== null) {
|
||||
variables.push(match[1].trim())
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
// Helper function to replace variables with FormatJS placeholders
|
||||
function replaceWithFormatJSPlaceholders(
|
||||
templateString: string,
|
||||
variables: string[]
|
||||
): string {
|
||||
let transformedMessage = templateString
|
||||
|
||||
variables.forEach((variable) => {
|
||||
transformedMessage = transformedMessage.replace(
|
||||
`\${${variable}}`,
|
||||
`{${variable}}`
|
||||
)
|
||||
})
|
||||
|
||||
return transformedMessage
|
||||
}
|
||||
|
||||
// Helper function to get the variables from the ternary branch
|
||||
function getVariableArguments(exp: Expression): string {
|
||||
const idProp = exp
|
||||
.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression)
|
||||
.getProperties()
|
||||
.find((prop) => "getName" in prop && prop.getName() === "id")
|
||||
?.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
|
||||
|
||||
if (idProp) {
|
||||
const extractedVariables = extractVariablesFromTemplateString(
|
||||
idProp.getText()
|
||||
)
|
||||
if (extractedVariables.length > 0) {
|
||||
return `{ ${extractedVariables.map((v) => `${v}: ${v}`).join(", ")} }`
|
||||
}
|
||||
}
|
||||
return "{}" // Return empty if no variables found
|
||||
}
|
||||
|
||||
// Helper function to transform object literals and return text
|
||||
function transformObjectLiteralAndReturn(object: Node): string {
|
||||
if (object.getKind() === ts.SyntaxKind.ObjectLiteralExpression) {
|
||||
const obj = object.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression)
|
||||
transformObjectLiteral(obj)
|
||||
return obj.getText() // Return the transformed object literal as text
|
||||
}
|
||||
return object.getText()
|
||||
}
|
||||
27
apps/scandic-web/codemods/lokalise/output.tsx
Normal file
27
apps/scandic-web/codemods/lokalise/output.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable formatjs/enforce-description */
|
||||
/* eslint-disable formatjs/enforce-default-message */
|
||||
/* eslint-disable formatjs/no-id */
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
export default async function ServerComponentWithIntl() {
|
||||
const intl = await getIntl()
|
||||
const variable = "some value"
|
||||
|
||||
return (
|
||||
<h1>
|
||||
{true
|
||||
? intl.formatMessage({
|
||||
defaultMessage: "Some string",
|
||||
description: {},
|
||||
})
|
||||
: intl.formatMessage(
|
||||
{
|
||||
defaultMessage: `Other string {variable}`,
|
||||
description: {},
|
||||
},
|
||||
{ variable: variable }
|
||||
)}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user