feat(SW-706): add Lokalise tooling and codemod
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "plugin:import/typescript"],
|
||||
"plugins": ["simple-import-sort", "@typescript-eslint"],
|
||||
"plugins": ["simple-import-sort", "@typescript-eslint", "formatjs"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
@@ -72,6 +72,18 @@
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"formatjs/enforce-description": ["warn", "anything"],
|
||||
"formatjs/enforce-default-message": ["error", "literal"],
|
||||
"formatjs/enforce-placeholders": ["error"],
|
||||
"formatjs/enforce-plural-rules": ["error"],
|
||||
"formatjs/no-literal-string-in-jsx": ["error"],
|
||||
"formatjs/no-multiple-whitespaces": ["error"],
|
||||
"formatjs/no-multiple-plurals": ["error"],
|
||||
"formatjs/no-invalid-icu": ["error"],
|
||||
"formatjs/no-id": ["error"],
|
||||
"formatjs/no-complex-selectors": ["error"],
|
||||
"formatjs/no-useless-message": ["error"],
|
||||
"formatjs/prefer-pound-in-plural": ["error"]
|
||||
}
|
||||
}
|
||||
|
||||
5
apps/scandic-web/.gitignore
vendored
5
apps/scandic-web/.gitignore
vendored
@@ -58,3 +58,8 @@ variables.css
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
.yarn/releases
|
||||
|
||||
# i18n generated files
|
||||
i18n/tooling/extracted.json
|
||||
i18n/tooling/translations/
|
||||
.swc
|
||||
|
||||
@@ -12,5 +12,7 @@ netlify.toml
|
||||
package.json
|
||||
package-lock.json
|
||||
.gitignore
|
||||
**/fixtures/**
|
||||
**/expectations/**
|
||||
*.bicep
|
||||
*.ico
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -16,11 +16,11 @@ For this to work we must set the language with `setLang` on the topmost layout
|
||||
|
||||
This was inspired by [server-only-context](https://github.com/manvalls/server-only-context)
|
||||
|
||||
## Translations a.k.a. UI labels
|
||||
## Translations a.k.a. UI labels a.k.a. Lokalise
|
||||
|
||||
### Quickstart: How to use react-intl in the codebase
|
||||
|
||||
> These recommendations are temporary. They will be updated once support for Lokalise lands.
|
||||
> For more information read about [the workflow below](#markdown-header-the-workflow).
|
||||
|
||||
- **Do not destructure `formatMessage` from either `getIntl()` or `useIntl()`.**
|
||||
|
||||
@@ -52,9 +52,9 @@ This was inspired by [server-only-context](https://github.com/manvalls/server-on
|
||||
const message = intl.formatMessage(...)
|
||||
```
|
||||
|
||||
- **Do not pass variables as id.**
|
||||
- **Do not pass variables as defaultMessage.**
|
||||
|
||||
The id needs to be literal string so that the tooling can properly find and extract defined messages.
|
||||
The `defaultMessage` needs to be a literal string so that the tooling can properly find and extract defined messages. Do not use template strings with variable interpolation either.
|
||||
|
||||
❌ Do not do this:
|
||||
|
||||
@@ -62,7 +62,11 @@ This was inspired by [server-only-context](https://github.com/manvalls/server-on
|
||||
const data = await getSomeData()
|
||||
...
|
||||
const message = intl.formatMessage({
|
||||
id: data.type,
|
||||
defaultMessage: data.type,
|
||||
})
|
||||
...
|
||||
const message = intl.formatMessage({
|
||||
defaultMessage: `Certification: ${data.type}`,
|
||||
})
|
||||
```
|
||||
|
||||
@@ -70,24 +74,32 @@ This was inspired by [server-only-context](https://github.com/manvalls/server-on
|
||||
|
||||
This is a hard one to give a general solution/rule for, but in essence it should use either use a "switch approach" or `defineMessage/defineMessages` ([docs](https://formatjs.github.io/docs/react-intl/api/#definemessagesdefinemessage)) in some way.
|
||||
|
||||
The most common reason for this is the data contains one or several words we want translated.
|
||||
The most common reason for this scenario is that the data contains one or several words/sentences we want translated.
|
||||
|
||||
The preferred solution is to use a "switch approach" (or something equivalent), checking some property of the entity to decide what message to use. It is explicit about what cases are supported (this helps finding bugs by lowering complexity by making it easier to find where a string is used), the declaration is coupled with the usage (keeping our messages up to date and clutter free over time), the formatjs eslint plugin helps enforce proper usage and TS will force us to keep this list up to date with the typings of `data.type`. Preferably ALL data points of this manner should be an enum or an array of strings `as const` (or equivalent) for the best TS support here.
|
||||
The preferred solution is to use a "switch approach" (or something equivalent), checking some property of the entity to decide what message to use.
|
||||
|
||||
- It is explicit about what cases are supported: this helps finding bugs by lowering complexity by making it easier to find where a string is used.
|
||||
- The declaration is coupled with the usage: keeping our messages up to date and clutter free over time.
|
||||
- The formatjs eslint plugin helps enforce proper usage.
|
||||
- TypeScript will force us to keep the list up to date with the typings of `data.type`. Preferably ALL data points of this manner should be an enum or an array of strings `as const` (or equivalent) for the best TS support here.
|
||||
|
||||
```typescript
|
||||
...
|
||||
const data = await getSomeData()
|
||||
...
|
||||
let message = intl.formatMessage({id: 'N/A'})) // or some other default/"no match" message
|
||||
let message = intl.formatMessage({defaultMessage: 'N/A'})) // or some other default/"no match" message
|
||||
switch (data.type) {
|
||||
case "Restaurant":
|
||||
message = intl.formatMessage({id: "Restaurant"})
|
||||
message = intl.formatMessage({defaultMessage: "Restaurant"})
|
||||
break;
|
||||
case "Bar":
|
||||
message = intl.formatMessage({id: "Bar"})
|
||||
message = intl.formatMessage({defaultMessage: "Bar"})
|
||||
break;
|
||||
case "Pool":
|
||||
message = intl.formatMessage({id: "Pool"})
|
||||
message = intl.formatMessage({defaultMessage: "Pool"})
|
||||
break;
|
||||
case "Some certification":
|
||||
message = intl.formatMessage({defaultMessage: "Certification: {name}"}, { name: "Some certification"})
|
||||
break;
|
||||
default:
|
||||
// TS will throw if it reaches here if typings for `data.type` are properly defined and the above cases are exhaustive.
|
||||
@@ -97,124 +109,93 @@ This was inspired by [server-only-context](https://github.com/manvalls/server-on
|
||||
}
|
||||
```
|
||||
|
||||
If the above is not possible the escape hatch is using something like the following.
|
||||
If the above is not possible the escape hatch is using something like the following. Avoid using this because:
|
||||
|
||||
Avoid using this as this decouples the message declaration from message consumption causing stale messages to linger around and clutter up things. It also makes it a lot harder to find where in the code a string is being used. The eslint plugin is also unable to enforce placeholders with this approach decreasing confidence in our messages and potentially hiding bugs.
|
||||
- This decouples the message declaration from the message consumption: causing stale messages to linger around and clutter up the codebase.
|
||||
- It makes it a lot harder to find where a string is being used in the codebase.
|
||||
- The formatjs eslint plugin is unable to enforce placeholders: this decreases confidence in our messages and potentially hiding bugs.
|
||||
|
||||
```typescript
|
||||
import { defineMessages } from "react-intl"
|
||||
...
|
||||
const data = await getSomeData()
|
||||
...
|
||||
defineMessage({
|
||||
id: "Restaurant",
|
||||
const restaurantMessage = defineMessage({
|
||||
defaultMessage: "Restaurant",
|
||||
})
|
||||
defineMessage({
|
||||
id: "Bar",
|
||||
const barMessage = defineMessage({
|
||||
defaultMessage: "Bar",
|
||||
})
|
||||
defineMessage({
|
||||
id: "Pool",
|
||||
const poolMessage = defineMessage({
|
||||
defaultMessage: "Pool",
|
||||
})
|
||||
// OR
|
||||
defineMessages({
|
||||
const messages = defineMessages({
|
||||
restaurant: {
|
||||
id: "Restaurant",
|
||||
defaultMessage: "Restaurant",
|
||||
},
|
||||
bar: {
|
||||
id: "Bar",
|
||||
defaultMessage: "Bar",
|
||||
},
|
||||
pool: {
|
||||
id: "Pool",
|
||||
defaultMessage: "Pool",
|
||||
}
|
||||
})
|
||||
// We do not use the return value of defineMessage(s)
|
||||
// The keys are also not used, can be anything really
|
||||
// defineMessages can be used instead of calling defineMessage multiple times.
|
||||
// Both approaches yield the exact same result!
|
||||
...
|
||||
return (
|
||||
<p>{intl.formatMessage(messages.restaurant)}</p> // or .bar or .pool, etc.
|
||||
)
|
||||
...
|
||||
|
||||
// Since calls to defineMessage/defineMessages get their messages extracted,
|
||||
// technically you can do the following instead of accessing the key like above.
|
||||
// But it is not encouraged as this decoupling leads to had to track usage and decay over time:
|
||||
const message = intl.formatMessage({
|
||||
// eslint-disable-next-line formatjs/enforce-default-message
|
||||
id: data.type, // data.type === "Restaurant" | "Bar" | "Pool"
|
||||
defaultMessage: data.type, // data.type === "Restaurant" | "Bar" | "Pool"
|
||||
})
|
||||
```
|
||||
|
||||
- **Do not use id key as an alias.**
|
||||
- **Do not use defaultMessage key as an alias.**
|
||||
|
||||
The id and the message need to be the same in all the dictionaries.
|
||||
|
||||
This prepares for a future codemod that will transform the ids into default messages.
|
||||
|
||||
❌ Do not do this:
|
||||
|
||||
```typescript
|
||||
// react
|
||||
const message = intl.formatMessage({
|
||||
id: "Some alias",
|
||||
})
|
||||
|
||||
// dictionary (en.json for example)
|
||||
...
|
||||
"Some alias": "The real message is here"
|
||||
...
|
||||
```
|
||||
|
||||
✅ instead do this:
|
||||
|
||||
```typescript
|
||||
// react
|
||||
const message = intl.formatMessage({
|
||||
id: "The real message is here",
|
||||
})
|
||||
|
||||
// dictionary (en.json for example)
|
||||
...
|
||||
"The real message is here": "The real message is here"
|
||||
...
|
||||
```
|
||||
|
||||
- **Add translations to all local JSON dictionaries.**
|
||||
|
||||
Even if the message is untranslated when adding it. Even if the id is used as a fallback when a translation is missing, the fallback does not get interpolated.
|
||||
The `defaultMessage` should be the actual message you want to display (and send to Lokalise). It must be written in American/US English.
|
||||
|
||||
❌ Do not do this:
|
||||
|
||||
```typescript
|
||||
const message = intl.formatMessage({
|
||||
id: "An id NOT added to all dictionaries",
|
||||
})
|
||||
const messageWithVariable = intl.formatMessage({
|
||||
id: "An id NOT added to all dictionaries with {someVariable}",
|
||||
defaultMessage: "some.alias",
|
||||
})
|
||||
```
|
||||
|
||||
✅ instead do this:
|
||||
|
||||
```typescript
|
||||
// react
|
||||
const message = intl.formatMessage({
|
||||
id: "An id added to all dictionaries",
|
||||
defaultMessage: "The real message is here in US English",
|
||||
})
|
||||
const messageWithVariable = intl.formatMessage({
|
||||
id: "An id added to all dictionaries with {someVariable}",
|
||||
```
|
||||
|
||||
- **Do not use conditionals when defining messages.**
|
||||
|
||||
❌ Do not do this:
|
||||
|
||||
```typescript
|
||||
const message = intl.formatMessage({
|
||||
defaultMessage: someVariable ? "Variable is truthy" : "Variables i falsey",
|
||||
})
|
||||
```
|
||||
|
||||
// dictionary: en.json (original messages)
|
||||
...
|
||||
"An id added to all dictionaries": "An id added to all dictionaries",
|
||||
"An id added to all dictionaries with {someVariable}": "An id added to all dictionaries with {someVariable}",
|
||||
...
|
||||
✅ instead do this:
|
||||
|
||||
// dictionary: sv.json (translated messages)
|
||||
...
|
||||
"An id added to all dictionaries": "Ett id tillagt i alla filer för uppslag",
|
||||
"An id added to all dictionaries with {someVariable}": "Ett id tillagt i alla filer för uppslag med {someVariable}",
|
||||
...
|
||||
|
||||
// dictionary: de.json (untranslated messages, still required)
|
||||
...
|
||||
"An id added to all dictionaries": "An id added to all dictionaries",
|
||||
"An id added to all dictionaries with {someVariable}": "An id added to all dictionaries with {someVariable}",
|
||||
...
|
||||
```typescript
|
||||
const truthyMessage = intl.formatMessage({
|
||||
defaultMessage: "Variable is truthy",
|
||||
})
|
||||
const falseyMessage = intl.formatMessage({
|
||||
defaultMessage: "Variable is falsey",
|
||||
})
|
||||
const message = someVariable ? truthyMessage : falseyMessage
|
||||
```
|
||||
|
||||
- **Avoid using ICU special words as placeholder names**
|
||||
@@ -241,7 +222,7 @@ This was inspired by [server-only-context](https://github.com/manvalls/server-on
|
||||
...
|
||||
const message = intl.formatMessage(
|
||||
{
|
||||
id: "The number is {number}",
|
||||
defaultMessage: "The number is {number}",
|
||||
},
|
||||
{
|
||||
number,
|
||||
@@ -255,7 +236,7 @@ This was inspired by [server-only-context](https://github.com/manvalls/server-on
|
||||
const goodNameForVarButNotForPlaceholder = getValueSomeWay()
|
||||
...
|
||||
const message = intl.formatMessage({
|
||||
id: "The number is {goodNameForVarButNotForPlaceholder}",
|
||||
defaultMessage: "The number is {goodNameForVarButNotForPlaceholder}",
|
||||
}, {
|
||||
goodNameForVarButNotForPlaceholder
|
||||
})
|
||||
@@ -270,7 +251,7 @@ This was inspired by [server-only-context](https://github.com/manvalls/server-on
|
||||
...
|
||||
const message = intl.formatMessage(
|
||||
{
|
||||
id: "The number is {membershipNumber}",
|
||||
defaultMessage: "The number is {membershipNumber}",
|
||||
},
|
||||
{
|
||||
membershipNumber: goodNameForVarButNotForPlaceholder,
|
||||
@@ -285,10 +266,146 @@ This was inspired by [server-only-context](https://github.com/manvalls/server-on
|
||||
...
|
||||
const message = intl.formatMessage(
|
||||
{
|
||||
id: "The number is {value}",
|
||||
defaultMessage: "The number is {value}",
|
||||
},
|
||||
{
|
||||
value: number,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
- Do not give id to messages.
|
||||
|
||||
The eslint plugin will automatically fix this (removes the id on save/fix).
|
||||
|
||||
❌ Do not do this:
|
||||
|
||||
```typescript
|
||||
const message = intl.formatMessage({
|
||||
id: "some-id",
|
||||
defaultMessage: "This is a message",
|
||||
})
|
||||
```
|
||||
|
||||
✅ instead do this:
|
||||
|
||||
```typescript
|
||||
const message = intl.formatMessage({
|
||||
defaultMessage: "This is a message",
|
||||
})
|
||||
```
|
||||
|
||||
### Integration with Lokalise
|
||||
|
||||
> For more information read about [the workflow below](#markdown-header-the-workflow).
|
||||
|
||||
#### Message extraction from codebase
|
||||
|
||||
Extracts the messages from calls to `intl.formatMessage()` and other supported methods on `intl`.
|
||||
|
||||
Running the following command will generate a JSON file at `./i18n/tooling/extracted.json`. The format of this file is for consumption by Lokalise. This JSON file is what gets uploaded to Lokalise.
|
||||
|
||||
```bash
|
||||
npm run i18n:extract
|
||||
```
|
||||
|
||||
#### Message upload to Lokalise
|
||||
|
||||
Set the environment variable `LOKALISE_API_KEY` to the API key for Lokalise.
|
||||
|
||||
Running the following command will upload the JSON file, that was generated by extraction, to Lokalise.
|
||||
|
||||
It supports the different upload phases from Lokalise meaning that once this command completes the messages are available for translation in Lokalise.
|
||||
|
||||
```bash
|
||||
npm run i18n:upload
|
||||
```
|
||||
|
||||
#### Message download from Lokalise
|
||||
|
||||
Set the environment variable `LOKALISE_API_KEY` to the API key for Lokalise.
|
||||
|
||||
Running the following command will download the translated assets from Lokalise to your local working copy.
|
||||
|
||||
_DOCUMENTATION PENDING FOR FULL WORKFLOW._
|
||||
|
||||
```bash
|
||||
npm run i18n:download
|
||||
```
|
||||
|
||||
#### Message compilation
|
||||
|
||||
Compiles the assets that were downloaded from Lokalise into the dictionaries used by the codebase.
|
||||
|
||||
_DOCUMENTATION PENDING FOR FULL WORKFLOW._
|
||||
|
||||
```bash
|
||||
npm run i18n:compile
|
||||
```
|
||||
|
||||
### The workflow
|
||||
|
||||
We use the following technical stack to handle translations of UI labels.
|
||||
|
||||
- [react-intl](https://formatjs.io/docs/getting-started/installation/): Library for handling translations in the codebase.
|
||||
- [Lokalise](https://lokalise.com/): TMS (Translations Management System) for handling the translations from the editor side.
|
||||
|
||||
A translation is usually called a "message" in the context of i18n with react-intl.
|
||||
|
||||
In the codebase we use the [Imperative API](https://formatjs.github.io/docs/react-intl/api/) of react-intl. This allows us to use the same patterns and rules regardless of where we are formatting messages (JSX, data, utilities, etc). We do not use the [React components](https://formatjs.github.io/docs/react-intl/components/) of react-intl for the same reason, they would only work in JSX and would possibly differ in implementation and patterns with other parts of the code.
|
||||
|
||||
To define messages we primarily invoke `intl.formatMessage` (but `intl` has other methods for other purposes too!). We take care not to name the message, we do that by **not** passing the `id` attribute to `formatMessage`. The reason for this is that we also have implemented the [@formatjs/cli](https://formatjs.io/docs/tooling/cli) and the SWC plugin. Due to the SWC plugin being a fairly new project and also due to version mismatching reasons, we are using a pinned version of the SWC plugin. Once we upgrade to Next.js 15 we can upgrade the SWC plugin too and skip pinning it. Together, these two are responsible for allowing us to extract defined messages in our codebase. This optimizes the developer workflow by freeing up developers from having to name things and to not be wary of duplicates/collisions as they will be handled by the extraction tool and Lokalise.
|
||||
|
||||
Example of a simple message:
|
||||
|
||||
```typescript
|
||||
const myMessage = intl.formatMessage({
|
||||
defaultMessage: "Hello from the docs!",
|
||||
})
|
||||
```
|
||||
|
||||
In cases where extra information is helpful to the translators, e.g. short sentences which are hard to translate without context or we are dealing with homographs (words that are spelled the same but have different meanings), we can also specify a `description` key in the `formatMessage` call. This allows the tooling to extract all the different permutations of the declared message along with their respective descriptions. The same sentence/word will show up multiple times in Lokalise with different contexts, allowing them to be translated indivudually. The description is intended to assist translators using Lokalise by providing context or additional information. The value is an object with the following structure:
|
||||
|
||||
```typescript
|
||||
description = string | {
|
||||
context?: string // natural language string providing context for translators in Lokalise (optional)
|
||||
limit?: number // character limit for the key enforced by Lokalise (optional)
|
||||
tags?: string // comma separated string (optional)
|
||||
}
|
||||
```
|
||||
|
||||
Examples of a homograph with different context:
|
||||
|
||||
```typescript
|
||||
const myMessage1 = intl.formatMessage({
|
||||
defaultMessage: "Book",
|
||||
description: "The action to reserve a room",
|
||||
})
|
||||
const myMessage2 = intl.formatMessage({
|
||||
defaultMessage: "Book",
|
||||
description: "A physical book that you can read",
|
||||
})
|
||||
```
|
||||
|
||||
Examples with a (contrived) sentence:
|
||||
|
||||
```typescript
|
||||
const myMessage1 = intl.formatMessage({
|
||||
defaultMessage: "He gave her a ring!",
|
||||
description: "A man used a phone to call a woman",
|
||||
})
|
||||
const myMessage2 = intl.formatMessage({
|
||||
defaultMessage: "He gave her a ring!",
|
||||
description: "A man gave a woman a piece of jewelry",
|
||||
})
|
||||
```
|
||||
|
||||
#### Diagram
|
||||
|
||||
A diagram showing the high level workflow of translations. It is currently a manual process syncing messages to and from Lokalise into the codebase.
|
||||
|
||||
[](https://mermaid.live/edit#pako:eNqdVM1u2zAMfhVC5_YFcthhaw8bug5Ye2rcA2PRthBZMiRqWVD03UfFipwtbVBMF1HkJ_Ljj_SiWq9JrVRn_a4dMDDc_Wxc40BWTJs-4DRAGwiZYP1l3jP2eYbkdUO_yPqJQpzt60YtqkY9w_U1aOqMowgjxYi9CMYBDwQ5-gYjCeYTfDa8Se2WuPqpGnEzBySnz-jRbw7YMqxvi_AuwQJ4i-GILqG1ewjJQWyDmRgYQ0-yDcjHIBdSQKchTdajjhAoJsvG9fDt4cc9sIc7v0VrSqbHw8LnqLmYqIBdtIdWPFbxn2RvtWEfYrWL76Iqie582EbYGV78Ge_iX7xOb3-ImXFMImVmX6v4bhsq5H8aof3OzUWuneiCH5cCCxd_YbhOg5_PV17n0MyLg-l7oQmbZKw-dFuTtHtfipkmLUh9XtR7Ymu6_WnconqrpOWt5Ytl5AqkzHY21DmYTctYZGNtxWxUV2qkMKLR8spfsq5RUpxR-rkSUWPYNqpxr4LDxP5h71q14pDoSgWf-kGtOrRRTnN-NwbF-Vi1E7on75czHWbt-_ypHP6W1z--V4Yq)
|
||||
|
||||
The following is a diagram showing how all the parts interact with each other. Note that interacting with Lokalise in any way does **NOT** trigger a build and deploy. A manual action by a developer is required to deploy the latest translations from Lokalise. (Once the manual process reaches maturity we might try and automate it)
|
||||
|
||||
[](https://mermaid.live/edit#pako:eNqdUsFqwzAM_RXh8_IDYewwusvoOlhvIxc1VhITxwq2vCyU_vuchKVlLRvMF0uypaf3eEdVsiaVq8ryUDboBbZvhYN0nrQR9gGyDAb2bYDBSAPi0QWLYtgFuM-yB9hyi9YEWro29EGWe1oaNVXGUYCOQsA6BcaBNAQT6AEDwTTg0cghli3JrQkduojWjuCjg1B60wsI-prS1aAAfaaNSvkFAp2G2FtGHeB5_7oD4XVnuCZw8fQnuObBLYNX9Mpzd55hXAK7Inxm-B_GJXe9sZeifq9B-gf8DXXXdIISb-p6gj1EY_WslKYk1Th37kisqcalT92pjnyHRiezHKdaoRKxjgqVp1CjbwtVuFP6h1F4P7pS5eIj3SnPsW5UXqENKYu9RqGNwdpjt1Z7dO_M55xm870s3pwtevoCSUvs9A)
|
||||
|
||||
11
apps/scandic-web/i18n/tooling/download.ts
Normal file
11
apps/scandic-web/i18n/tooling/download.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import path from "node:path"
|
||||
|
||||
import { download } from "./lokalise"
|
||||
|
||||
const extractPath = path.resolve(__dirname, "translations")
|
||||
|
||||
async function main() {
|
||||
await download(extractPath)
|
||||
}
|
||||
|
||||
main()
|
||||
10
apps/scandic-web/i18n/tooling/formatter.mjs
Normal file
10
apps/scandic-web/i18n/tooling/formatter.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
// Run the formatter.ts through Jiti
|
||||
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import createJiti from "jiti"
|
||||
|
||||
const formatter = createJiti(fileURLToPath(import.meta.url))("./formatter.ts")
|
||||
|
||||
export const format = formatter.format
|
||||
export const compile = formatter.compile
|
||||
99
apps/scandic-web/i18n/tooling/formatter.ts
Normal file
99
apps/scandic-web/i18n/tooling/formatter.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// https://docs.lokalise.com/en/articles/3229161-structured-json
|
||||
|
||||
import type { LokaliseMessageDescriptor } from "@/types/intl"
|
||||
|
||||
type TranslationEntry = {
|
||||
translation: string
|
||||
notes?: string
|
||||
context?: string
|
||||
limit?: number
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
type CompiledEntries = Record<string, string>
|
||||
|
||||
type LokaliseStructuredJson = Record<string, TranslationEntry>
|
||||
|
||||
export function format(
|
||||
msgs: LokaliseMessageDescriptor[]
|
||||
): LokaliseStructuredJson {
|
||||
const results: LokaliseStructuredJson = {}
|
||||
for (const [id, msg] of Object.entries(msgs)) {
|
||||
const { defaultMessage, description } = msg
|
||||
|
||||
if (typeof defaultMessage === "string") {
|
||||
const entry: TranslationEntry = {
|
||||
translation: defaultMessage,
|
||||
}
|
||||
|
||||
if (description) {
|
||||
if (typeof description === "string") {
|
||||
console.warn(
|
||||
`Unsupported type for description, expected 'object', got ${typeof context}. Skipping!`,
|
||||
msg
|
||||
)
|
||||
} else {
|
||||
const { context, limit, tags } = description
|
||||
|
||||
if (context) {
|
||||
if (typeof context === "string") {
|
||||
entry.context = context
|
||||
} else {
|
||||
console.warn(
|
||||
`Unsupported type for context, expected 'string', got ${typeof context}`,
|
||||
msg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
if (limit && typeof limit === "number") {
|
||||
entry.limit = limit
|
||||
} else {
|
||||
console.warn(
|
||||
`Unsupported type for limit, expected 'number', got ${typeof limit}`,
|
||||
msg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
if (tags && typeof tags === "string") {
|
||||
const tagArray = tags.split(",").map((s) => s.trim())
|
||||
if (tagArray.length) {
|
||||
entry.tags = tagArray
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`Unsupported type for tags, expected Array, got ${typeof tags}`,
|
||||
msg
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results[id] = entry
|
||||
} else {
|
||||
console.warn(
|
||||
`Skipping message, unsupported type for defaultMessage, expected string, got ${typeof defaultMessage}`,
|
||||
{
|
||||
id,
|
||||
msg,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export function compile(msgs: LokaliseStructuredJson): CompiledEntries {
|
||||
const results: CompiledEntries = {}
|
||||
|
||||
for (const [id, msg] of Object.entries(msgs)) {
|
||||
results[id] = msg.translation
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
210
apps/scandic-web/i18n/tooling/lokalise.ts
Normal file
210
apps/scandic-web/i18n/tooling/lokalise.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import fs from "node:fs/promises"
|
||||
import { performance, PerformanceObserver } from "node:perf_hooks"
|
||||
|
||||
import { LokaliseApi } from "@lokalise/node-api"
|
||||
import AdmZip from "adm-zip"
|
||||
|
||||
const projectId = "4194150766ff28c418f010.39532200"
|
||||
const lokaliseApi = new LokaliseApi({ apiKey: process.env.LOKALISE_API_KEY })
|
||||
|
||||
function log(msg: string, ...args: any[]) {
|
||||
console.log(`[lokalise] ${msg}`, ...args)
|
||||
}
|
||||
|
||||
function error(msg: string, ...args: any[]) {
|
||||
console.error(`[lokalise] ${msg}`, ...args)
|
||||
}
|
||||
|
||||
let resolvePerf: (value?: unknown) => void
|
||||
const performanceMetrics = new Promise((resolve) => {
|
||||
resolvePerf = resolve
|
||||
})
|
||||
|
||||
const perf = new PerformanceObserver((items) => {
|
||||
const entries = items.getEntries()
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "done") {
|
||||
// This is the last measure meant for clean up
|
||||
performance.clearMarks()
|
||||
perf.disconnect()
|
||||
if (typeof resolvePerf === "function") {
|
||||
resolvePerf()
|
||||
}
|
||||
} else {
|
||||
log(`[metrics] ${entry.name} completed in ${entry.duration} ms`)
|
||||
}
|
||||
}
|
||||
performance.clearMeasures()
|
||||
})
|
||||
perf.observe({ type: "measure" })
|
||||
|
||||
async function waitUntilUploadDone(processId: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
performance.mark("waitUntilUploadDoneStart")
|
||||
|
||||
log("Checking upload status...")
|
||||
|
||||
performance.mark("getProcessStart")
|
||||
const process = await lokaliseApi.queuedProcesses().get(processId, {
|
||||
project_id: projectId,
|
||||
})
|
||||
performance.mark("getProcessEnd")
|
||||
performance.measure(
|
||||
"Get Queued Process",
|
||||
"getProcessStart",
|
||||
"getProcessEnd"
|
||||
)
|
||||
|
||||
log(`Status: ${process.status}`)
|
||||
|
||||
if (process.status === "finished") {
|
||||
clearInterval(interval)
|
||||
performance.mark("waitUntilUploadDoneEnd", { detail: "success" })
|
||||
performance.measure(
|
||||
"Wait on upload",
|
||||
"waitUntilUploadDoneStart",
|
||||
"waitUntilUploadDoneEnd"
|
||||
)
|
||||
resolve()
|
||||
} else if (process.status === "failed") {
|
||||
throw process
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(interval)
|
||||
error("An error occurred:", e)
|
||||
performance.mark("waitUntilUploadDoneEnd", { detail: error })
|
||||
performance.measure(
|
||||
"Wait on upload",
|
||||
"waitUntilUploadDoneStart",
|
||||
"waitUntilUploadDoneEnd"
|
||||
)
|
||||
reject()
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
export async function upload(filepath: string) {
|
||||
try {
|
||||
log(`Uploading ${filepath}...`)
|
||||
|
||||
performance.mark("uploadStart")
|
||||
|
||||
performance.mark("sourceFileReadStart")
|
||||
const data = await fs.readFile(filepath, "utf8")
|
||||
const buff = Buffer.from(data, "utf8")
|
||||
const base64 = buff.toString("base64")
|
||||
performance.mark("sourceFileReadEnd")
|
||||
performance.measure(
|
||||
"Read source file",
|
||||
"sourceFileReadStart",
|
||||
"sourceFileReadEnd"
|
||||
)
|
||||
|
||||
performance.mark("lokaliseUploadInitStart")
|
||||
const bgProcess = await lokaliseApi.files().upload(projectId, {
|
||||
data: base64,
|
||||
filename: "en.json",
|
||||
lang_iso: "en",
|
||||
detect_icu_plurals: true,
|
||||
format: "json",
|
||||
convert_placeholders: true,
|
||||
replace_modified: true,
|
||||
})
|
||||
performance.mark("lokaliseUploadInitEnd")
|
||||
performance.measure(
|
||||
"Upload init",
|
||||
"lokaliseUploadInitStart",
|
||||
"lokaliseUploadInitEnd"
|
||||
)
|
||||
|
||||
performance.mark("lokaliseUploadStart")
|
||||
await waitUntilUploadDone(bgProcess.process_id)
|
||||
performance.mark("lokaliseUploadEnd")
|
||||
performance.measure(
|
||||
"Upload transfer",
|
||||
"lokaliseUploadStart",
|
||||
"lokaliseUploadEnd"
|
||||
)
|
||||
|
||||
log("Upload successful")
|
||||
} catch (e) {
|
||||
error("Upload failed", e)
|
||||
} finally {
|
||||
performance.mark("uploadEnd")
|
||||
|
||||
performance.measure("Upload operation", "uploadStart", "uploadEnd")
|
||||
}
|
||||
|
||||
performance.measure("done")
|
||||
|
||||
await performanceMetrics
|
||||
}
|
||||
|
||||
export async function download(extractPath: string) {
|
||||
try {
|
||||
log("Downloading translations...")
|
||||
|
||||
performance.mark("downloadStart")
|
||||
|
||||
performance.mark("lokaliseDownloadInitStart")
|
||||
const downloadResponse = await lokaliseApi.files().download(projectId, {
|
||||
format: "json_structured",
|
||||
indentation: "2sp",
|
||||
placeholder_format: "icu",
|
||||
plural_format: "icu",
|
||||
icu_numeric: true,
|
||||
bundle_structure: "%LANG_ISO%.%FORMAT%",
|
||||
directory_prefix: "",
|
||||
filter_data: ["last_reviewed_only"],
|
||||
export_empty_as: "skip",
|
||||
})
|
||||
performance.mark("lokaliseDownloadInitEnd")
|
||||
performance.measure(
|
||||
"Download init",
|
||||
"lokaliseDownloadInitStart",
|
||||
"lokaliseDownloadInitEnd"
|
||||
)
|
||||
|
||||
const { bundle_url } = downloadResponse
|
||||
|
||||
performance.mark("lokaliseDownloadStart")
|
||||
const bundleResponse = await fetch(bundle_url)
|
||||
performance.mark("lokaliseDownloadEnd")
|
||||
performance.measure(
|
||||
"Download transfer",
|
||||
"lokaliseDownloadStart",
|
||||
"lokaliseDownloadEnd"
|
||||
)
|
||||
|
||||
if (bundleResponse.ok) {
|
||||
performance.mark("unpackTranslationsStart")
|
||||
const arrayBuffer = await bundleResponse.arrayBuffer()
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer))
|
||||
const zip = new AdmZip(buffer)
|
||||
zip.extractAllTo(extractPath, true)
|
||||
performance.mark("unpackTranslationsEnd")
|
||||
performance.measure(
|
||||
"Unpacking translations",
|
||||
"unpackTranslationsStart",
|
||||
"unpackTranslationsEnd"
|
||||
)
|
||||
|
||||
log("Download successful")
|
||||
} else {
|
||||
throw bundleResponse
|
||||
}
|
||||
} catch (e) {
|
||||
error("Download failed", e)
|
||||
} finally {
|
||||
performance.mark("downloadEnd")
|
||||
|
||||
performance.measure("Download operation", "downloadStart", "downloadEnd")
|
||||
}
|
||||
|
||||
performance.measure("done")
|
||||
|
||||
await performanceMetrics
|
||||
}
|
||||
11
apps/scandic-web/i18n/tooling/upload.ts
Normal file
11
apps/scandic-web/i18n/tooling/upload.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import path from "node:path"
|
||||
|
||||
import { upload } from "./lokalise"
|
||||
|
||||
const filepath = path.resolve(__dirname, "./extracted.json")
|
||||
|
||||
async function main() {
|
||||
await upload(filepath)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -1,14 +1,14 @@
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import createJiti from "jiti"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
import { findMyBooking } from "./constants/routes/findMyBooking.js"
|
||||
import { login, logout } from "./constants/routes/handleAuth.js"
|
||||
import { myPages } from "./constants/routes/myPages.js"
|
||||
import { myStay } from "./constants/routes/myStay.js"
|
||||
|
||||
const path = fileURLToPath(new URL(import.meta.url))
|
||||
const jiti = createJiti(path)
|
||||
const jiti = createJiti(fileURLToPath(import.meta.url))
|
||||
jiti("./env/server")
|
||||
jiti("./env/client")
|
||||
|
||||
@@ -33,6 +33,14 @@ const nextConfig = {
|
||||
"*.scandichotels.com",
|
||||
],
|
||||
},
|
||||
swcPlugins: [
|
||||
[
|
||||
"@formatjs/swc-plugin-experimental",
|
||||
{
|
||||
ast: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
images: {
|
||||
minimumCacheTTL: 2678400, // 31 days
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"ci:build": "yarn lint && yarn test && yarn build",
|
||||
"clean": "rm -rf .next"
|
||||
"clean": "rm -rf .next",
|
||||
"i18n:extract": "formatjs extract \"{actions,app,components,constants,hooks,i18n,lib,middlewares,server,stores,utils}/**/*.{ts,tsx}\" --format i18n/tooling/formatter.mjs --out-file i18n/tooling/extracted.json",
|
||||
"i18n:upload": "jiti i18n/tooling/upload.ts",
|
||||
"i18n:download": "jiti i18n/tooling/download.ts",
|
||||
"i18n:compile": "formatjs compile-folder --ast --format i18n/tooling/formatter.mjs i18n/tooling/translations i18n/dictionaries2",
|
||||
"i18n:sync": "yarn i18n:extract && yarn i18n:upload && yarn i18n:download && yarn i18n:compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.27",
|
||||
@@ -104,11 +109,15 @@
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.6.1",
|
||||
"@formatjs/swc-plugin-experimental": "0.4.0",
|
||||
"@lokalise/node-api": "^14.0.0",
|
||||
"@scandic-hotels/typescript-config": "workspace:*",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@testing-library/jest-dom": "^6.4.6",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/json-stable-stringify-without-jsonify": "^1.0.2",
|
||||
"@types/node": "^20",
|
||||
@@ -116,10 +125,12 @@
|
||||
"@types/react-dom": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
||||
"@typescript-eslint/parser": "^8.17.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cypress": "^13.6.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"eslint-plugin-formatjs": "^5.2.14",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.0",
|
||||
"jest": "^29.7.0",
|
||||
@@ -133,6 +144,7 @@
|
||||
"react-material-symbols": "^4.4.0",
|
||||
"schema-dts": "^1.1.2",
|
||||
"start-server-and-test": "^2.0.3",
|
||||
"ts-morph": "^25.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "5.4.5",
|
||||
"typescript-plugin-css-modules": "^5.1.0"
|
||||
|
||||
16
apps/scandic-web/types/intl.d.ts
vendored
Normal file
16
apps/scandic-web/types/intl.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
// import type { MessageDescriptor } from "@formatjs/intl"
|
||||
|
||||
// Module augmentation
|
||||
declare module "@formatjs/intl" {
|
||||
// We are unable to override description field on MessageDescriptor from formatjs.
|
||||
// Module augmentation does not allow for that. But we leave it here for reference.
|
||||
// Instead we export our own LokaliseMessageDescriptor and use that where we control the code.
|
||||
// For example in our custom formatter in i18n/formatter.ts
|
||||
// interface MessageDescriptor {
|
||||
// description?: {
|
||||
// context?: string
|
||||
// limit?: number
|
||||
// tags?: string[]
|
||||
// }
|
||||
// }
|
||||
}
|
||||
10
apps/scandic-web/types/intl.ts
Normal file
10
apps/scandic-web/types/intl.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { MessageDescriptor } from "@formatjs/intl"
|
||||
|
||||
export interface LokaliseMessageDescriptor
|
||||
extends Omit<MessageDescriptor, "description"> {
|
||||
description: {
|
||||
context?: string
|
||||
limit?: number
|
||||
tags?: string
|
||||
}
|
||||
}
|
||||
339
yarn.lock
339
yarn.lock
@@ -2085,6 +2085,41 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/cli@npm:^6.6.1":
|
||||
version: 6.6.1
|
||||
resolution: "@formatjs/cli@npm:6.6.1"
|
||||
peerDependencies:
|
||||
"@glimmer/env": ^0.1.7
|
||||
"@glimmer/reference": ^0.91.1 || ^0.92.0 || ^0.93.0
|
||||
"@glimmer/syntax": ^0.92.0 || ^0.93.0
|
||||
"@glimmer/validator": ^0.92.0 || ^0.93.0
|
||||
"@vue/compiler-core": ^3.4.0
|
||||
content-tag: ^2.0.1 || ^3.0.0
|
||||
ember-template-recast: ^6.1.4
|
||||
vue: ^3.4.0
|
||||
peerDependenciesMeta:
|
||||
"@glimmer/env":
|
||||
optional: true
|
||||
"@glimmer/reference":
|
||||
optional: true
|
||||
"@glimmer/syntax":
|
||||
optional: true
|
||||
"@glimmer/validator":
|
||||
optional: true
|
||||
"@vue/compiler-core":
|
||||
optional: true
|
||||
content-tag:
|
||||
optional: true
|
||||
ember-template-recast:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
bin:
|
||||
formatjs: bin/formatjs
|
||||
checksum: 10c0/c2fa01a836a3ad99df4cdd37e83a6729941df05ad9a37432cfcb8a3bc8fbe41a87707d127c8c0094803494d7dda26746df4fa637937476ce032b0fd84101e201
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/ecma402-abstract@npm:2.2.4":
|
||||
version: 2.2.4
|
||||
resolution: "@formatjs/ecma402-abstract@npm:2.2.4"
|
||||
@@ -2228,6 +2263,35 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/swc-plugin-experimental@npm:0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "@formatjs/swc-plugin-experimental@npm:0.4.0"
|
||||
peerDependencies:
|
||||
"@swc/core": ^1.6.0
|
||||
checksum: 10c0/6ccc7c6e3de5100e11a77839c9f5976efd6a641c8307b43f495856b1a58b0e09b779e851434897f349c7319881d42fed7fb41eb02e70307c75bb9d1c8c23e854
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/ts-transformer@npm:3.13.32":
|
||||
version: 3.13.32
|
||||
resolution: "@formatjs/ts-transformer@npm:3.13.32"
|
||||
dependencies:
|
||||
"@formatjs/icu-messageformat-parser": "npm:2.11.1"
|
||||
"@types/json-stable-stringify": "npm:1"
|
||||
"@types/node": "npm:14 || 16 || 17 || 18 || 20 || 22"
|
||||
chalk: "npm:4"
|
||||
json-stable-stringify: "npm:1"
|
||||
tslib: "npm:2"
|
||||
typescript: "npm:5"
|
||||
peerDependencies:
|
||||
ts-jest: 27 || 28 || 29
|
||||
peerDependenciesMeta:
|
||||
ts-jest:
|
||||
optional: true
|
||||
checksum: 10c0/f51102e5427f9e22ed8dd4094e0347fa7bf96b99a97c5b48eb23bb11cd319f653c829073040884d0c6381136e227a1d0d2b7b2ac3ec0dba6ed9e26f454b61254
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@graphql-typed-document-node/core@npm:^3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "@graphql-typed-document-node/core@npm:3.2.0"
|
||||
@@ -2875,6 +2939,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lokalise/node-api@npm:^14.0.0":
|
||||
version: 14.0.0
|
||||
resolution: "@lokalise/node-api@npm:14.0.0"
|
||||
checksum: 10c0/277648c6b8a926dd5168db0278bb80e8a3f2012e7ca313e0c1e8bb351821743e36f4f660dec1f8ff768bbda4fd87de3749f29880e554eaa757a8405718149403
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mdx-js/react@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "@mdx-js/react@npm:3.1.0"
|
||||
@@ -6281,10 +6352,13 @@ __metadata:
|
||||
dependencies:
|
||||
"@azure/monitor-opentelemetry-exporter": "npm:^1.0.0-beta.27"
|
||||
"@contentstack/live-preview-utils": "npm:^3.0.0"
|
||||
"@formatjs/cli": "npm:^6.6.1"
|
||||
"@formatjs/intl": "npm:^2.10.15"
|
||||
"@formatjs/swc-plugin-experimental": "npm:0.4.0"
|
||||
"@hookform/error-message": "npm:^2.0.1"
|
||||
"@hookform/resolvers": "npm:^3.3.4"
|
||||
"@internationalized/date": "npm:^3.6.0"
|
||||
"@lokalise/node-api": "npm:^14.0.0"
|
||||
"@netlify/blobs": "npm:^8.1.0"
|
||||
"@netlify/functions": "npm:^3.0.0"
|
||||
"@netlify/plugin-nextjs": "npm:^5.9.4"
|
||||
@@ -6316,6 +6390,7 @@ __metadata:
|
||||
"@trpc/react-query": "npm:^11.0.1"
|
||||
"@trpc/server": "npm:^11.0.1"
|
||||
"@tsparticles/confetti": "npm:^3.5.0"
|
||||
"@types/adm-zip": "npm:^0.5.7"
|
||||
"@types/geojson": "npm:^7946.0.16"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/json-stable-stringify-without-jsonify": "npm:^1.0.2"
|
||||
@@ -6327,6 +6402,7 @@ __metadata:
|
||||
"@typescript-eslint/parser": "npm:^8.17.0"
|
||||
"@vercel/otel": "npm:^1.9.1"
|
||||
"@vis.gl/react-google-maps": "npm:^1.2.0"
|
||||
adm-zip: "npm:^0.5.16"
|
||||
class-variance-authority: "npm:^0.7.0"
|
||||
clean-deep: "npm:^3.4.0"
|
||||
contentstack: "npm:^3.23.0"
|
||||
@@ -6340,6 +6416,7 @@ __metadata:
|
||||
embla-carousel-react: "npm:^8.5.2"
|
||||
eslint: "npm:^8"
|
||||
eslint-config-next: "npm:^14.0.4"
|
||||
eslint-plugin-formatjs: "npm:^5.2.14"
|
||||
eslint-plugin-import: "npm:^2.29.1"
|
||||
eslint-plugin-simple-import-sort: "npm:^12.1.0"
|
||||
fast-deep-equal: "npm:^3.1.3"
|
||||
@@ -6385,6 +6462,7 @@ __metadata:
|
||||
start-server-and-test: "npm:^2.0.3"
|
||||
supercluster: "npm:^8.0.1"
|
||||
superjson: "npm:^2.2.1"
|
||||
ts-morph: "npm:^25.0.1"
|
||||
ts-node: "npm:^10.9.2"
|
||||
typescript: "npm:5.4.5"
|
||||
typescript-plugin-css-modules: "npm:^5.1.0"
|
||||
@@ -7583,6 +7661,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ts-morph/common@npm:~0.26.0":
|
||||
version: 0.26.1
|
||||
resolution: "@ts-morph/common@npm:0.26.1"
|
||||
dependencies:
|
||||
fast-glob: "npm:^3.3.2"
|
||||
minimatch: "npm:^9.0.4"
|
||||
path-browserify: "npm:^1.0.1"
|
||||
checksum: 10c0/49d33162de3f09fb2a242e38c04a7fa30966977a5282d97d0e27c1d68541956e917ac4de9aac6b41182dbae188482665da6e919c3f6ef0dcb90cea24e4d637e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsconfig/node10@npm:^1.0.7":
|
||||
version: 1.0.11
|
||||
resolution: "@tsconfig/node10@npm:1.0.11"
|
||||
@@ -7867,6 +7956,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/adm-zip@npm:^0.5.7":
|
||||
version: 0.5.7
|
||||
resolution: "@types/adm-zip@npm:0.5.7"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10c0/6ba62bd8f4a6e7ffdad08d951c65c4f69161c2b96cc34249dcf3c448dca85749409407b297472d5c66b711325de89012e8607b74c87d99ec23e9b7a44b6c71c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/argparse@npm:1.0.38":
|
||||
version: 1.0.38
|
||||
resolution: "@types/argparse@npm:1.0.38"
|
||||
@@ -7973,6 +8071,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/eslint@npm:9":
|
||||
version: 9.6.1
|
||||
resolution: "@types/eslint@npm:9.6.1"
|
||||
dependencies:
|
||||
"@types/estree": "npm:*"
|
||||
"@types/json-schema": "npm:*"
|
||||
checksum: 10c0/69ba24fee600d1e4c5abe0df086c1a4d798abf13792d8cfab912d76817fe1a894359a1518557d21237fbaf6eda93c5ab9309143dee4c59ef54336d1b3570420e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/estree@npm:*, @types/estree@npm:1.0.6, @types/estree@npm:^1.0.0":
|
||||
version: 1.0.6
|
||||
resolution: "@types/estree@npm:1.0.6"
|
||||
@@ -8073,6 +8181,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json-schema@npm:*":
|
||||
version: 7.0.15
|
||||
resolution: "@types/json-schema@npm:7.0.15"
|
||||
checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json-stable-stringify-without-jsonify@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "@types/json-stable-stringify-without-jsonify@npm:1.0.2"
|
||||
@@ -8080,6 +8195,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json-stable-stringify@npm:1":
|
||||
version: 1.1.0
|
||||
resolution: "@types/json-stable-stringify@npm:1.1.0"
|
||||
checksum: 10c0/8f69944701510243cd3a83aa44363a8a4d366f11a659b258f69fb3ad0f94ab1e2533206a2c929ac7fd18784d201b663b3f02a45934f545c926f051d8cb4df095
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json5@npm:^0.0.29":
|
||||
version: 0.0.29
|
||||
resolution: "@types/json5@npm:0.0.29"
|
||||
@@ -8121,6 +8243,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:14 || 16 || 17 || 18 || 20 || 22":
|
||||
version: 22.13.10
|
||||
resolution: "@types/node@npm:22.13.10"
|
||||
dependencies:
|
||||
undici-types: "npm:~6.20.0"
|
||||
checksum: 10c0/a3865f9503d6f718002374f7b87efaadfae62faa499c1a33b12c527cfb9fd86f733e1a1b026b80c5a0e4a965701174bc3305595a7d36078aa1abcf09daa5dee9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^20":
|
||||
version: 20.17.19
|
||||
resolution: "@types/node@npm:20.17.19"
|
||||
@@ -8170,6 +8301,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/picomatch@npm:3":
|
||||
version: 3.0.2
|
||||
resolution: "@types/picomatch@npm:3.0.2"
|
||||
checksum: 10c0/f35d16fe10a6e13ead6499dd7d7d317e4fd78e48260398104e837e5ca83d393024bdc6f432cb644c0a69b0726a071fcc6eb09befbbcfafb3c3c5f71dbbfde487
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/postcss-modules-local-by-default@npm:^4.0.2":
|
||||
version: 4.0.2
|
||||
resolution: "@types/postcss-modules-local-by-default@npm:4.0.2"
|
||||
@@ -8407,6 +8545,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.20.0":
|
||||
version: 8.20.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.20.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.20.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.20.0"
|
||||
checksum: 10c0/a8074768d06c863169294116624a45c19377ff0b8635ad5fa4ae673b43cf704d1b9b79384ceef0ff0abb78b107d345cd90fe5572354daf6ad773fe462ee71e6a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.24.1":
|
||||
version: 8.24.1
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.24.1"
|
||||
@@ -8457,6 +8605,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.20.0":
|
||||
version: 8.20.0
|
||||
resolution: "@typescript-eslint/types@npm:8.20.0"
|
||||
checksum: 10c0/21292d4ca089897015d2bf5ab99909a7b362902f63f4ba10696676823b50d00c7b4cd093b4b43fba01d12bc3feca3852d2c28528c06d8e45446b7477887dbee7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.24.1":
|
||||
version: 8.24.1
|
||||
resolution: "@typescript-eslint/types@npm:8.24.1"
|
||||
@@ -8471,6 +8626,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.20.0":
|
||||
version: 8.20.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.20.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.20.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.20.0"
|
||||
debug: "npm:^4.3.4"
|
||||
fast-glob: "npm:^3.3.2"
|
||||
is-glob: "npm:^4.0.3"
|
||||
minimatch: "npm:^9.0.4"
|
||||
semver: "npm:^7.6.0"
|
||||
ts-api-utils: "npm:^2.0.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <5.8.0"
|
||||
checksum: 10c0/54a2c1da7d1c5f7e865b941e8a3c98eb4b5f56ed8741664a84065173bde9602cdb8866b0984b26816d6af885c1528311c11e7286e869ed424483b74366514cbd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.24.1":
|
||||
version: 8.24.1
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.24.1"
|
||||
@@ -8507,6 +8680,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.20.0":
|
||||
version: 8.20.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.20.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.4.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.20.0"
|
||||
"@typescript-eslint/types": "npm:8.20.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.20.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <5.8.0"
|
||||
checksum: 10c0/dd36c3b22a2adde1e1462aed0c8b4720f61859b4ebb0c3ef935a786a6b1cb0ec21eb0689f5a8debe8db26d97ebb979bab68d6f8fe7b0098e6200a485cfe2991b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.24.1":
|
||||
version: 8.24.1
|
||||
resolution: "@typescript-eslint/utils@npm:8.24.1"
|
||||
@@ -8537,6 +8725,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.20.0":
|
||||
version: 8.20.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.20.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.20.0"
|
||||
eslint-visitor-keys: "npm:^4.2.0"
|
||||
checksum: 10c0/e95d8b2685e8beb6637bf2e9d06e4177a400d3a2b142ba749944690f969ee3186b750082fd9bf34ada82acf1c5dd5970201dfd97619029c8ecca85fb4b50dbd8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.24.1":
|
||||
version: 8.24.1
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.24.1"
|
||||
@@ -8930,6 +9128,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"adm-zip@npm:^0.5.16":
|
||||
version: 0.5.16
|
||||
resolution: "adm-zip@npm:0.5.16"
|
||||
checksum: 10c0/6f10119d4570c7ba76dcf428abb8d3f69e63f92e51f700a542b43d4c0130373dd2ddfc8f85059f12d4a843703a90c3970cfd17876844b4f3f48bf042bfa6b49f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"agent-base@npm:6":
|
||||
version: 6.0.2
|
||||
resolution: "agent-base@npm:6.0.2"
|
||||
@@ -9979,6 +10184,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:4, chalk@npm:^4.0.0, chalk@npm:^4.1.0":
|
||||
version: 4.1.2
|
||||
resolution: "chalk@npm:4.1.2"
|
||||
dependencies:
|
||||
ansi-styles: "npm:^4.1.0"
|
||||
supports-color: "npm:^7.1.0"
|
||||
checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^2.4.2":
|
||||
version: 2.4.2
|
||||
resolution: "chalk@npm:2.4.2"
|
||||
@@ -9990,16 +10205,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^4.0.0, chalk@npm:^4.1.0":
|
||||
version: 4.1.2
|
||||
resolution: "chalk@npm:4.1.2"
|
||||
dependencies:
|
||||
ansi-styles: "npm:^4.1.0"
|
||||
supports-color: "npm:^7.1.0"
|
||||
checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^5.3.0, chalk@npm:^5.4.1":
|
||||
version: 5.4.1
|
||||
resolution: "chalk@npm:5.4.1"
|
||||
@@ -10244,6 +10449,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"code-block-writer@npm:^13.0.3":
|
||||
version: 13.0.3
|
||||
resolution: "code-block-writer@npm:13.0.3"
|
||||
checksum: 10c0/87db97b37583f71cfd7eced8bf3f0a0a0ca53af912751a734372b36c08cd27f3e8a4878ec05591c0cd9ae11bea8add1423e132d660edd86aab952656dd41fd66
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"codsen-utils@npm:^1.6.4":
|
||||
version: 1.6.4
|
||||
resolution: "codsen-utils@npm:1.6.4"
|
||||
@@ -11430,6 +11642,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"emoji-regex@npm:10.3.0":
|
||||
version: 10.3.0
|
||||
resolution: "emoji-regex@npm:10.3.0"
|
||||
checksum: 10c0/b4838e8dcdceb44cf47f59abe352c25ff4fe7857acaf5fb51097c427f6f75b44d052eb907a7a3b86f86bc4eae3a93f5c2b7460abe79c407307e6212d65c91163
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"emoji-regex@npm:^10.3.0":
|
||||
version: 10.4.0
|
||||
resolution: "emoji-regex@npm:10.4.0"
|
||||
@@ -11931,6 +12150,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-formatjs@npm:^5.2.14":
|
||||
version: 5.2.14
|
||||
resolution: "eslint-plugin-formatjs@npm:5.2.14"
|
||||
dependencies:
|
||||
"@formatjs/icu-messageformat-parser": "npm:2.11.1"
|
||||
"@formatjs/ts-transformer": "npm:3.13.32"
|
||||
"@types/eslint": "npm:9"
|
||||
"@types/picomatch": "npm:3"
|
||||
"@typescript-eslint/utils": "npm:8.20.0"
|
||||
magic-string: "npm:^0.30.0"
|
||||
picomatch: "npm:2 || 3 || 4"
|
||||
tslib: "npm:2"
|
||||
unicode-emoji-utils: "npm:^1.2.0"
|
||||
peerDependencies:
|
||||
eslint: 9
|
||||
checksum: 10c0/6d0b40cebb5e3e7ca426831b610963f02d36ec37aab784756e701a454f43193106cae4919138a77d3ba0229549214eef4e06f008b466c706bf475a40e575db9e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-import@npm:^2.28.1, eslint-plugin-import@npm:^2.29.1":
|
||||
version: 2.31.0
|
||||
resolution: "eslint-plugin-import@npm:2.31.0"
|
||||
@@ -15008,6 +15246,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"json-stable-stringify@npm:1":
|
||||
version: 1.2.1
|
||||
resolution: "json-stable-stringify@npm:1.2.1"
|
||||
dependencies:
|
||||
call-bind: "npm:^1.0.8"
|
||||
call-bound: "npm:^1.0.3"
|
||||
isarray: "npm:^2.0.5"
|
||||
jsonify: "npm:^0.0.1"
|
||||
object-keys: "npm:^1.1.1"
|
||||
checksum: 10c0/e623e7ce89282f089d56454087edb717357e8572089b552fbc6980fb7814dc3943f7d0e4f1a19429a36ce9f4428b6c8ee6883357974457aaaa98daba5adebeea
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"json-stringify-safe@npm:5, json-stringify-safe@npm:~5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "json-stringify-safe@npm:5.0.1"
|
||||
@@ -15048,6 +15299,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsonify@npm:^0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "jsonify@npm:0.0.1"
|
||||
checksum: 10c0/7f5499cdd59a0967ed35bda48b7cec43d850bbc8fb955cdd3a1717bb0efadbe300724d5646de765bb7a99fc1c3ab06eb80d93503c6faaf99b4ff50a3326692f6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsonparse@npm:^1.2.0":
|
||||
version: 1.3.1
|
||||
resolution: "jsonparse@npm:1.3.1"
|
||||
@@ -17340,6 +17598,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"picomatch@npm:2 || 3 || 4, picomatch@npm:^4.0.2":
|
||||
version: 4.0.2
|
||||
resolution: "picomatch@npm:4.0.2"
|
||||
checksum: 10c0/7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "picomatch@npm:2.3.1"
|
||||
@@ -17347,13 +17612,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"picomatch@npm:^4.0.2":
|
||||
version: 4.0.2
|
||||
resolution: "picomatch@npm:4.0.2"
|
||||
checksum: 10c0/7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pidtree@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "pidtree@npm:0.6.0"
|
||||
@@ -20509,7 +20767,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-api-utils@npm:^2.0.1":
|
||||
"ts-api-utils@npm:^2.0.0, ts-api-utils@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "ts-api-utils@npm:2.0.1"
|
||||
peerDependencies:
|
||||
@@ -20525,6 +20783,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-morph@npm:^25.0.1":
|
||||
version: 25.0.1
|
||||
resolution: "ts-morph@npm:25.0.1"
|
||||
dependencies:
|
||||
"@ts-morph/common": "npm:~0.26.0"
|
||||
code-block-writer: "npm:^13.0.3"
|
||||
checksum: 10c0/4e8b67d7fc521d5dcc7e4e6dfb2e10bc63fcaaef8c31b91d3c9eb98eacd42db70bde3359075dcb8e988a5510f02e2ee6f98a02f89045d648bfda72a5207d78ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-node@npm:^10.9.2":
|
||||
version: 10.9.2
|
||||
resolution: "ts-node@npm:10.9.2"
|
||||
@@ -20841,6 +21109,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:5, typescript@npm:^5.7.2, typescript@npm:^5.7.3":
|
||||
version: 5.8.2
|
||||
resolution: "typescript@npm:5.8.2"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 10c0/5c4f6fbf1c6389b6928fe7b8fcd5dc73bb2d58cd4e3883f1d774ed5bd83b151cbac6b7ecf11723de56d4676daeba8713894b1e9af56174f2f9780ae7848ec3c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:5.4.5":
|
||||
version: 5.4.5
|
||||
resolution: "typescript@npm:5.4.5"
|
||||
@@ -20861,13 +21139,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:^5.7.2, typescript@npm:^5.7.3":
|
||||
"typescript@patch:typescript@npm%3A5#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.7.2#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.7.3#optional!builtin<compat/typescript>":
|
||||
version: 5.8.2
|
||||
resolution: "typescript@npm:5.8.2"
|
||||
resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin<compat/typescript>::version=5.8.2&hash=5786d5"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 10c0/5c4f6fbf1c6389b6928fe7b8fcd5dc73bb2d58cd4e3883f1d774ed5bd83b151cbac6b7ecf11723de56d4676daeba8713894b1e9af56174f2f9780ae7848ec3c6
|
||||
checksum: 10c0/5448a08e595cc558ab321e49d4cac64fb43d1fa106584f6ff9a8d8e592111b373a995a1d5c7f3046211c8a37201eb6d0f1566f15cdb7a62a5e3be01d087848e2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -20891,16 +21169,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@npm%3A^5.7.2#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.7.3#optional!builtin<compat/typescript>":
|
||||
version: 5.8.2
|
||||
resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin<compat/typescript>::version=5.8.2&hash=5786d5"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 10c0/5448a08e595cc558ab321e49d4cac64fb43d1fa106584f6ff9a8d8e592111b373a995a1d5c7f3046211c8a37201eb6d0f1566f15cdb7a62a5e3be01d087848e2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typical@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "typical@npm:4.0.0"
|
||||
@@ -20972,6 +21240,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unicode-emoji-utils@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "unicode-emoji-utils@npm:1.2.0"
|
||||
dependencies:
|
||||
emoji-regex: "npm:10.3.0"
|
||||
checksum: 10c0/224413cab5f915abbbbf3e6061878f3c1b67acf7c6ab1d4bf283f13d290677633d614a7fd58b7af8cec54dc3a4e4f51c01f4797caa23c7c83cdaa759fe6de9ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unicode-match-property-ecmascript@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "unicode-match-property-ecmascript@npm:2.0.0"
|
||||
|
||||
Reference in New Issue
Block a user