From 931a53120ffc8683da8e19442e0bae0ce7679d1c Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko Date: Thu, 23 Feb 2023 12:13:42 +0100 Subject: [PATCH] ref(macro): split js (core) and jsx (react) macro --- packages/cli/src/api/extractors/babel.ts | 4 +- packages/core/package.json | 19 +- packages/core/src/macro/index.ts | 94 ++++++ .../src => core/src/macro}/macroJs.test.ts | 2 +- .../{macro/src => core/src/macro}/macroJs.ts | 32 +- packages/core/src/macro/types.test-d.tsx | 209 ++++++++++++ packages/core/src/macro/types.ts | 194 +++++++++++ packages/macro-lib/package.json | 40 +++ .../{macro => macro-lib}/src/constants.ts | 0 packages/{macro => macro-lib}/src/icu.test.ts | 6 +- packages/{macro => macro-lib}/src/icu.ts | 13 +- packages/macro-lib/src/index.ts | 4 + packages/macro-lib/src/types.ts | 7 + packages/macro-lib/src/utils.ts | 72 ++++ packages/macro/global.d.ts | 211 ------------ packages/macro/index.d.ts | 308 +----------------- packages/macro/index.js | 2 +- packages/macro/index.test-d.tsx | 306 ++--------------- packages/macro/package.json | 13 +- packages/macro/src/index.ts | 180 +--------- packages/macro/src/utils.ts | 4 - packages/macro/test/index.ts | 6 +- packages/react/package.json | 29 +- packages/react/src/macro/index.ts | 91 ++++++ .../src => react/src/macro}/macroJsx.test.ts | 2 +- .../src => react/src/macro}/macroJsx.ts | 22 +- packages/react/src/macro/types.test-d.tsx | 110 +++++++ packages/react/src/macro/types.ts | 106 ++++++ tsconfig.json | 5 +- 29 files changed, 1078 insertions(+), 1013 deletions(-) create mode 100644 packages/core/src/macro/index.ts rename packages/{macro/src => core/src/macro}/macroJs.test.ts (99%) rename packages/{macro/src => core/src/macro}/macroJs.ts (96%) create mode 100644 packages/core/src/macro/types.test-d.tsx create mode 100644 packages/core/src/macro/types.ts create mode 100644 packages/macro-lib/package.json rename packages/{macro => macro-lib}/src/constants.ts (100%) rename packages/{macro => macro-lib}/src/icu.test.ts (85%) rename packages/{macro => macro-lib}/src/icu.ts (93%) create mode 100644 packages/macro-lib/src/index.ts create mode 100644 packages/macro-lib/src/types.ts create mode 100644 packages/macro-lib/src/utils.ts delete mode 100644 packages/macro/global.d.ts delete mode 100644 packages/macro/src/utils.ts create mode 100644 packages/react/src/macro/index.ts rename packages/{macro/src => react/src/macro}/macroJsx.test.ts (99%) rename packages/{macro/src => react/src/macro}/macroJsx.ts (97%) create mode 100644 packages/react/src/macro/types.test-d.tsx create mode 100644 packages/react/src/macro/types.ts diff --git a/packages/cli/src/api/extractors/babel.ts b/packages/cli/src/api/extractors/babel.ts index 22f8b916c..aeb84d8c8 100644 --- a/packages/cli/src/api/extractors/babel.ts +++ b/packages/cli/src/api/extractors/babel.ts @@ -4,8 +4,8 @@ import type { ExtractPluginOpts } from "@lingui/babel-plugin-extract-messages" import linguiExtractMessages from "@lingui/babel-plugin-extract-messages" import type { ExtractorType } from "@lingui/conf" -import { ParserPlugin } from "@babel/parser" -import { LinguiMacroOpts } from "@lingui/macro/src" +import type { ParserPlugin } from "@babel/parser" +import type { LinguiMacroOpts } from "@lingui/macro-lib" const babelRe = new RegExp( "\\.(" + diff --git a/packages/core/package.json b/packages/core/package.json index 64e57b87a..8d85e10c3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -61,6 +61,23 @@ "dependencies": { "@babel/runtime": "^7.20.13", "@messageformat/parser": "^5.0.0", - "make-plural": "^6.2.2" + "make-plural": "^6.2.2", + "@lingui/conf": "3.17.1", + "@lingui/macro-lib": "3.17.1", + "@lingui/cli": "3.17.1" + }, + "peerDependencies": { + "babel-plugin-macros": "2 || 3" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + }, + "devDependencies": { + "@types/babel-plugin-macros": "^2.8.5", + "@babel/traverse":"^7.20.13", + "@babel/types": "^7.20.7", + "babel-plugin-macros": "^3.1.0" } } diff --git a/packages/core/src/macro/index.ts b/packages/core/src/macro/index.ts new file mode 100644 index 000000000..b081197e7 --- /dev/null +++ b/packages/core/src/macro/index.ts @@ -0,0 +1,94 @@ +import { createMacro, MacroParams } from "babel-plugin-macros" +import { getConfig as loadConfig, LinguiConfigNormalized } from "@lingui/conf" + +import { MacroJs } from "./macroJs" +import type { NodePath } from "@babel/traverse" +import { + addImport, + isRootPath, + throwNonMacroContextError, +} from "@lingui/macro-lib" + +export type LinguiMacroOpts = { + // explicitly set by CLI when running extraction process + extract?: boolean + linguiConfig?: LinguiConfigNormalized +} + +export const jsMacroTags = new Set([ + "defineMessage", + "arg", + "t", + "plural", + "select", + "selectOrdinal", +]) + +let config: LinguiConfigNormalized + +function getConfig(_config?: LinguiConfigNormalized) { + if (_config) { + config = _config + } + if (!config) { + config = loadConfig() + } + return config +} + +export function macro({ references, state, babel, config }: MacroParams) { + const opts: LinguiMacroOpts = config as LinguiMacroOpts + + const { i18nImportModule, i18nImportName } = getConfig( + opts.linguiConfig + ).runtimeConfigModule + + const jsNodes = new Set() + let needsI18nImport = false + + Object.keys(references).forEach((tagName) => { + const nodes = references[tagName] + + if (jsMacroTags.has(tagName)) { + nodes.forEach((node) => { + jsNodes.add(node.parentPath) + }) + } + // else { + // throw nodes[0].buildCodeFrameError(`Unknown macro ${tagName}`) + // } + }) + + const stripNonEssentialProps = + process.env.NODE_ENV == "production" && !opts.extract + + const jsNodesArray = Array.from(jsNodes) + + jsNodesArray.filter(isRootPath(jsNodesArray)).forEach((path) => { + const macro = new MacroJs(babel, { i18nImportName, stripNonEssentialProps }) + if (macro.replacePath(path)) needsI18nImport = true + }) + + if (needsI18nImport) { + addImport( + babel.types, + state.file.path.node, + i18nImportModule, + i18nImportName + ) + } +} + +;[...jsMacroTags].forEach((name) => { + Object.defineProperty(module.exports, name, { + get() { + throwNonMacroContextError("@lingui/core/macro") + }, + }) +}) + +export default createMacro(macro, { + configName: "lingui", +}) + +export * from "./types" diff --git a/packages/macro/src/macroJs.test.ts b/packages/core/src/macro/macroJs.test.ts similarity index 99% rename from packages/macro/src/macroJs.test.ts rename to packages/core/src/macro/macroJs.test.ts index 474a8c16f..62907d741 100644 --- a/packages/macro/src/macroJs.test.ts +++ b/packages/core/src/macro/macroJs.test.ts @@ -1,6 +1,6 @@ import { parseExpression } from "@babel/parser" import * as types from "@babel/types" -import MacroJs from "./macroJs" +import { MacroJs } from "./macroJs" import { CallExpression } from "@babel/types" function createMacro() { diff --git a/packages/macro/src/macroJs.ts b/packages/core/src/macro/macroJs.ts similarity index 96% rename from packages/macro/src/macroJs.ts rename to packages/core/src/macro/macroJs.ts index b8f6a8b42..bcfec87a4 100644 --- a/packages/macro/src/macroJs.ts +++ b/packages/core/src/macro/macroJs.ts @@ -1,9 +1,8 @@ -import * as babelTypes from "@babel/types" -import { +import type * as babelTypes from "@babel/types" +import type { CallExpression, Expression, Identifier, - isObjectProperty, Node, ObjectExpression, ObjectProperty, @@ -11,16 +10,22 @@ import { StringLiteral, TemplateLiteral, } from "@babel/types" -import { NodePath } from "@babel/traverse" +import type { NodePath } from "@babel/traverse" -import ICUMessageFormat, { +import { + COMMENT, + CONTEXT, + EXTRACT_MARK, + ID, + MESSAGE, + ICUMessageFormat, ArgToken, ParsedResult, TextToken, Token, -} from "./icu" -import { makeCounter } from "./utils" -import { COMMENT, CONTEXT, EXTRACT_MARK, ID, MESSAGE } from "./constants" + makeCounter, +} from "@lingui/macro-lib" + import { generateMessageId } from "@lingui/cli/api" const keepSpaceRe = /(?:\\(?:\r\n|\r|\n))+\s+/g @@ -35,7 +40,7 @@ export type MacroJsOpts = { stripNonEssentialProps: boolean } -export default class MacroJs { +export class MacroJs { // Babel Types types: typeof babelTypes @@ -99,7 +104,7 @@ export default class MacroJs { const i18nInstance = path.node.arguments[0] const tokens = this.tokenizeNode(path.parentPath.node) - const messageFormat = new ICUMessageFormat() + const messageFormat = new ICUMessageFormat(this.types) const { message: messageRaw, values } = messageFormat.fromTokens(tokens) const message = normalizeWhitespace(messageRaw) @@ -136,7 +141,7 @@ export default class MacroJs { const tokens = this.tokenizeNode(path.node) - const messageFormat = new ICUMessageFormat() + const messageFormat = new ICUMessageFormat(this.types) const { message: messageRaw, values } = messageFormat.fromTokens(tokens) const message = normalizeWhitespace(messageRaw) @@ -228,7 +233,7 @@ export default class MacroJs { let messageNode = messageProperty.value as StringLiteral if (tokens) { - const messageFormat = new ICUMessageFormat() + const messageFormat = new ICUMessageFormat(this.types) const { message: messageRaw, values } = messageFormat.fromTokens(tokens) const message = normalizeWhitespace(messageRaw) messageNode = this.types.stringLiteral(message) @@ -450,7 +455,8 @@ export default class MacroJs { ): ObjectProperty { return objectExp.properties.find( (property) => - isObjectProperty(property) && this.isIdentifier(property.key, key) + this.types.isObjectProperty(property) && + this.isIdentifier(property.key, key) ) as ObjectProperty } diff --git a/packages/core/src/macro/types.test-d.tsx b/packages/core/src/macro/types.test-d.tsx new file mode 100644 index 000000000..e7a8486f5 --- /dev/null +++ b/packages/core/src/macro/types.test-d.tsx @@ -0,0 +1,209 @@ +import { expectType } from "tsd" +import type { MessageDescriptor, I18n } from "@lingui/core" + +import { t, defineMessage, plural, selectOrdinal, select } from "." + +const name = "Jack" +const i18n: I18n = null + +// simple +expectType(t`Hello world`) +expectType(t`Hello ${name}`) + +// with custom i18n +expectType(t(i18n)`With custom i18n instance`) +expectType(t(i18n)`With custom i18n instance ${name}`) + +// with macro message descriptor +expectType( + t({ + id: "custom.id", + comment: "Hello", + context: "context", + message: "Hello world", + }) +) + +// only id +expectType(t({ id: "custom.id" })) + +// only message +expectType(t({ message: "my message" })) + +// @ts-expect-error no id or message +t({ comment: "", context: "" }) + +// @ts-expect-error id or message should be presented +t({}) + +// @ts-expect-error `values` is invalid field for macro message descriptor +t({ + id: "custom.id", + comment: "Hello", + context: "context", + message: "Hello world", + + values: {}, +}) + +// message descriptor + custom i18n +expectType( + t(i18n)({ + id: "custom.id", + comment: "Hello", + context: "context", + message: "Hello world", + }) +) + +expectType( + defineMessage({ + id: "custom.id", + comment: "Hello", + context: "context", + message: "Hello world", + }) +) + +// @ts-expect-error id or message should be presented +expectType(defineMessage({})) + +/////////////////// +//// Plural ////// +/////////////////// + +expectType( + plural("5", { + // @ts-expect-error extra properties are not allowed + incorrect: "", + + one: "...", + other: "...", + few: "...", + many: "...", + zero: "...", + }) +) + +expectType( + plural(5, { + one: "...", + other: "...", + }) +) + +// with offset +expectType( + plural(5, { + one: "...", + other: "...", + offset: 5, + }) +) + +// exact choices +expectType( + plural(5, { + 0: "...", + 1: "...", + one: "...", + other: "...", + }) +) + +expectType( + plural(5, { + // @ts-expect-error: should accept only strings + one: 5, + // @ts-expect-error: should accept only strings + other: 5, + }) +) + +/////////////////// +//// Select Ordinal +/////////////////// + +expectType( + selectOrdinal("5", { + // @ts-expect-error extra properties are not allowed + incorrect: "", + + one: "...", + other: "...", + few: "...", + many: "...", + zero: "...", + }) +) + +expectType( + selectOrdinal(5, { + one: "...", + other: "...", + }) +) + +// with offset +expectType( + selectOrdinal("5", { + one: "...", + other: "...", + offset: 5, + }) +) + +// exact choices +expectType( + selectOrdinal(5, { + 0: "...", + 1: "...", + one: "...", + other: "...", + }) +) + +expectType( + selectOrdinal(5, { + // @ts-expect-error: should accept only strings + one: 5, + // @ts-expect-error: should accept only strings + other: 5, + }) +) + +/////////////////// +//// Select +/////////////////// + +const gender = "male" +expectType( + select(gender, { + // todo: here is inconsistency between jsx macro and js. + // in JSX macro you should prefix exact choices with "_" + // but here is not. And it's better to use it with underscore, + // because this could be statically checked by TS + // type UnderscoreValue = `_${string}`; + male: "he", + female: "she", + other: "they", + }) +) + +expectType( + // @ts-expect-error value could be strings only + select(5, { + male: "he", + female: "she", + other: "they", + }) +) + +expectType( + select("male", { + // @ts-expect-error: should accept only strings + male: 5, + // @ts-expect-error: should accept only strings + other: 5, + }) +) diff --git a/packages/core/src/macro/types.ts b/packages/core/src/macro/types.ts new file mode 100644 index 000000000..85d6ed51b --- /dev/null +++ b/packages/core/src/macro/types.ts @@ -0,0 +1,194 @@ +import type { I18n, MessageDescriptor } from "@lingui/core" + +export type ChoiceOptions = { + /** Offset of value when calculating plural forms */ + offset?: number + zero?: string + one?: string + two?: string + few?: string + many?: string + + /** Catch-all option */ + other?: string + /** Exact match form, corresponds to =N rule */ + [digit: `${number}`]: string +} + +export type MacroMessageDescriptor = ( + | { + id: string + message?: string + } + | { + id?: string + message: string + } +) & { + comment?: string + context?: string +} + +/** + * Translates a message descriptor + * + * @example + * ``` + * import { t } from "@lingui/macro"; + * const message = t({ + * id: "msg.hello", + * comment: "Greetings at the homepage", + * message: `Hello ${name}`, + * }); + * ``` + * + * @example + * ``` + * import { t } from "@lingui/macro"; + * const message = t({ + * id: "msg.plural", + * message: plural(value, { one: "...", other: "..." }), + * }); + * ``` + * + * @param descriptor The message descriptor to translate + */ +export declare function t(descriptor: MacroMessageDescriptor): string + +/** + * Translates a template string using the global I18n instance + * + * @example + * ``` + * import { t } from "@lingui/macro"; + * const message = t`Hello ${name}`; + * ``` + */ +export declare function t( + literals: TemplateStringsArray, + ...placeholders: any[] +): string + +/** + * Translates a template string or message descriptor using a given I18n instance + * + * @example + * ``` + * import { t } from "@lingui/macro"; + * import { I18n } from "@lingui/core"; + * const i18n = new I18n({ + * locale: "nl", + * messages: { "Hello {0}": "Hallo {0}" }, + * }); + * const message = t(i18n)`Hello ${name}`; + * ``` + * + * @example + * ``` + * import { t } from "@lingui/macro"; + * import { I18n } from "@lingui/core"; + * const i18n = new I18n({ + * locale: "nl", + * messages: { "Hello {0}": "Hallo {0}" }, + * }); + * const message = t(i18n)({ message: `Hello ${name}` }); + * ``` + */ +export declare function t(i18n: I18n): { + (literals: TemplateStringsArray, ...placeholders: any[]): string + (descriptor: MacroMessageDescriptor): string +} + +/** + * Pluralize a message + * + * @example + * ``` + * import { plural } from "@lingui/macro"; + * const message = plural(count, { + * one: "# Book", + * other: "# Books", + * }); + * ``` + * + * @param value Determines the plural form + * @param options Object with available plural forms + */ +export declare function plural( + value: number | string, + options: ChoiceOptions +): string + +/** + * Pluralize a message using ordinal forms + * + * Similar to `plural` but instead of using cardinal plural forms, + * it uses ordinal forms. + * + * @example + * ``` + * import { selectOrdinal } from "@lingui/macro"; + * const message = selectOrdinal(count, { + * one: "#st", + * two: "#nd", + * few: "#rd", + * other: "#th", + * }); + * ``` + * + * @param value Determines the plural form + * @param options Object with available plural forms + */ +export declare function selectOrdinal( + value: number | string, + options: ChoiceOptions +): string + +export type SelectOptions = { + /** Catch-all option */ + other: string + [matches: string]: string +} + +/** + * Selects a translation based on a value + * + * Select works like a switch statement. It will + * select one of the forms in `options` object which + * key matches exactly `value`. + * + * @example + * ``` + * import { select } from "@lingui/macro"; + * const message = select(gender, { + * male: "he", + * female: "she", + * other: "they", + * }); + * ``` + * + * @param value The key of choices to use + * @param choices + */ +export declare function select(value: string, choices: SelectOptions): string + +/** + * Define a message for later use + * + * `defineMessage` can be used to add comments for translators, + * or to override the message ID. + * + * @example + * ``` + * import { defineMessage } from "@lingui/macro"; + * const message = defineMessage({ + * comment: "Greetings on the welcome page", + * message: `Welcome, ${name}!`, + * }); + * ``` + * + * @param descriptor The message descriptor + */ +export declare function defineMessage( + descriptor: MacroMessageDescriptor +): MessageDescriptor diff --git a/packages/macro-lib/package.json b/packages/macro-lib/package.json new file mode 100644 index 000000000..1e8327e1f --- /dev/null +++ b/packages/macro-lib/package.json @@ -0,0 +1,40 @@ +{ + "name": "@lingui/macro-lib", + "version": "3.17.1", + "description": "Utils for lingui macro", + "main": "./build/index.js", + "types": "./index.d.ts", + "scripts": { + "test:tsd": "tsd" + }, + "license": "MIT", + "keywords": [], + "repository": { + "type": "git", + "url": "https://github.com/lingui/js-lingui.git" + }, + "bugs": { + "url": "https://github.com/lingui/js-lingui/issues" + }, + "engines": { + "node": ">=14.0.0" + }, + "files": [ + "LICENSE", + "README.md", + "build/" + ], + "dependencies": { + "@babel/runtime": "^7.20.13" + }, + "devDependencies": { + "@lingui/conf": "^3.17.1", + "@babel/traverse": "^7.20.13", + "@babel/types": "^7.20.7" + }, + "tsd": { + "compilerOptions": { + "strict": false + } + } +} diff --git a/packages/macro/src/constants.ts b/packages/macro-lib/src/constants.ts similarity index 100% rename from packages/macro/src/constants.ts rename to packages/macro-lib/src/constants.ts diff --git a/packages/macro/src/icu.test.ts b/packages/macro-lib/src/icu.test.ts similarity index 85% rename from packages/macro/src/icu.test.ts rename to packages/macro-lib/src/icu.test.ts index 897803955..846492c81 100644 --- a/packages/macro/src/icu.test.ts +++ b/packages/macro-lib/src/icu.test.ts @@ -1,9 +1,9 @@ -import ICUMessageFormat, { Token } from "./icu" +import { ICUMessageFormat, Token } from "./icu" import { Identifier } from "@babel/types" describe("ICU MessageFormat", function () { it("should collect text message", function () { - const messageFormat = new ICUMessageFormat() + const messageFormat = new ICUMessageFormat({} as any) const tokens: Token[] = [ { type: "text", @@ -19,7 +19,7 @@ describe("ICU MessageFormat", function () { }) it("should collect text message with arguments", function () { - const messageFormat = new ICUMessageFormat() + const messageFormat = new ICUMessageFormat({} as any) const tokens: Token[] = [ { type: "text", diff --git a/packages/macro/src/icu.ts b/packages/macro-lib/src/icu.ts similarity index 93% rename from packages/macro/src/icu.ts rename to packages/macro-lib/src/icu.ts index 03c1496ca..fd85ec003 100644 --- a/packages/macro/src/icu.ts +++ b/packages/macro-lib/src/icu.ts @@ -1,9 +1,5 @@ -import { - Expression, - isJSXEmptyExpression, - JSXElement, - Node, -} from "@babel/types" +import type { Expression, JSXElement, Node } from "@babel/types" +import type * as babelTypes from "@babel/types" const metaOptions = ["id", "comment", "props"] @@ -44,7 +40,8 @@ export type ElementToken = { export type Tokens = Token | Token[] export type Token = TextToken | ArgToken | ElementToken -export default class ICUMessageFormat { +export class ICUMessageFormat { + constructor(private t: typeof babelTypes) {} public fromTokens(tokens: Tokens): ParsedResult { return (Array.isArray(tokens) ? tokens : [tokens]) .map((token) => this.processToken(token)) @@ -74,7 +71,7 @@ export default class ICUMessageFormat { } else if (token.type === "arg") { if ( token.value !== undefined && - isJSXEmptyExpression(token.value as Node) + this.t.isJSXEmptyExpression(token.value as Node) ) { return null } diff --git a/packages/macro-lib/src/index.ts b/packages/macro-lib/src/index.ts new file mode 100644 index 000000000..47a83c69f --- /dev/null +++ b/packages/macro-lib/src/index.ts @@ -0,0 +1,4 @@ +export * from "./utils" +export * from "./types" +export * from "./constants" +export * from "./icu" diff --git a/packages/macro-lib/src/types.ts b/packages/macro-lib/src/types.ts new file mode 100644 index 000000000..15c76402d --- /dev/null +++ b/packages/macro-lib/src/types.ts @@ -0,0 +1,7 @@ +import type { LinguiConfigNormalized } from "@lingui/conf" + +export type LinguiMacroOpts = { + // explicitly set by CLI when running extraction process + extract?: boolean + linguiConfig?: LinguiConfigNormalized +} diff --git a/packages/macro-lib/src/utils.ts b/packages/macro-lib/src/utils.ts new file mode 100644 index 000000000..eb9b19b18 --- /dev/null +++ b/packages/macro-lib/src/utils.ts @@ -0,0 +1,72 @@ +import type { NodePath } from "@babel/traverse" +import type { ImportDeclaration, Program } from "@babel/types" +import type * as babelTypes from "@babel/types" + +export const makeCounter = + (index = 0) => + () => + index++ + +/** + * Filtering nested macro calls + * + * + * <-- this would be filtered out + * + */ +export function isRootPath(allPath: NodePath[]) { + return (node: NodePath) => + (function traverse(path): boolean { + if (!path.parentPath) { + return true + } else { + return !allPath.includes(path.parentPath) && traverse(path.parentPath) + } + })(node) +} + +export function addImport( + t: typeof babelTypes, + program: Program, + module: string, + importName: string +) { + const linguiImport = program.body.find( + (importNode) => + t.isImportDeclaration(importNode) && + importNode.source.value === module && + // https://github.com/lingui/js-lingui/issues/777 + importNode.importKind !== "type" + ) as ImportDeclaration + + const tIdentifier = t.identifier(importName) + // Handle adding the import or altering the existing import + if (linguiImport) { + if ( + linguiImport.specifiers.findIndex( + (specifier) => + t.isImportSpecifier(specifier) && + t.isIdentifier(specifier.imported, { name: importName }) + ) === -1 + ) { + linguiImport.specifiers.push(t.importSpecifier(tIdentifier, tIdentifier)) + } + } else { + program.body.unshift( + t.importDeclaration( + [t.importSpecifier(tIdentifier, tIdentifier)], + t.stringLiteral(module) + ) + ) + } +} + +export function throwNonMacroContextError(packageName: string) { + throw new Error( + `The macro you imported from "${packageName}" is being executed outside the context of compilation with babel-plugin-macros. ` + + `This indicates that you don't have the babel plugin "babel-plugin-macros" or "@lingui/swc-plugin" configured correctly.` + + `\n Please see the documentation for how to configure babel-plugin-macros properly: ` + + "https://github.com/kentcdodds/babel-plugin-macros/blob/main/other/docs/user.md" + + `\n For SWC Version: https://lingui.dev/ref/swc-plugin` + ) +} diff --git a/packages/macro/global.d.ts b/packages/macro/global.d.ts deleted file mode 100644 index 08eadba66..000000000 --- a/packages/macro/global.d.ts +++ /dev/null @@ -1,211 +0,0 @@ -// read more about this file here -// https://github.com/lingui/js-lingui/issues/936 -// @ts-ignore -declare module "@lingui/macro" { - import type { MessageDescriptor, I18n } from "@lingui/core" - - export type BasicType = { - id?: string - comment?: string - } - - /** - * Translates a message descriptor - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * const message = t({ - * id: "msg.hello", - * comment: "Greetings at the homepage", - * message: `Hello ${name}`, - * }); - * ``` - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * const message = t({ - * id: "msg.plural", - * message: plural(value, { one: "...", other: "..." }), - * }); - * ``` - * - * @param descriptor The message descriptor to translate - */ - export function t(descriptor: MessageDescriptor): string - - /** - * Translates a template string using the global I18n instance - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * const message = t`Hello ${name}`; - * ``` - */ - export function t( - literals: TemplateStringsArray, - ...placeholders: any[] - ): string - - /** - * Translates a template string or message descriptor using a given I18n instance - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * import { I18n } from "@lingui/core"; - * const i18n = new I18n({ - * locale: "nl", - * messages: { "Hello {0}": "Hallo {0}" }, - * }); - * const message = t(i18n)`Hello ${name}`; - * ``` - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * import { I18n } from "@lingui/core"; - * const i18n = new I18n({ - * locale: "nl", - * messages: { "Hello {0}": "Hallo {0}" }, - * }); - * const message = t(i18n)({ message: `Hello ${name}` }); - * ``` - */ - export function t(i18n: I18n): { - (literals: TemplateStringsArray, ...placeholders: any[]): string - (descriptor: MessageDescriptor): string - } - - export type UnderscoreDigit = { [digit: string]: T } - export type ChoiceOptions = { - offset?: number - zero?: T - one?: T - few?: T - many?: T - other?: T - } & UnderscoreDigit - - /** - * Pluralize a message - * - * @example - * ``` - * import { plural } from "@lingui/macro"; - * const message = plural(count, { - * one: "# Book", - * other: "# Books", - * }); - * ``` - * - * @param value Determines the plural form - * @param options Object with available plural forms - */ - export function plural( - value: number | string, - options: ChoiceOptions & BasicType - ): string - - /** - * Pluralize a message using ordinal forms - * - * Similar to `plural` but instead of using cardinal plural forms, - * it uses ordinal forms. - * - * @example - * ``` - * import { selectOrdinal } from "@lingui/macro"; - * const message = selectOrdinal(count, { - * one: "#st", - * two: "#nd", - * few: "#rd", - * other: "#th", - * }); - * ``` - * - * @param value Determines the plural form - * @param options Object with available plural forms - */ - export function selectOrdinal( - value: number | string, - options: ChoiceOptions & BasicType - ): string - - /** - * Selects a translation based on a value - * - * Select works like a switch statement. It will - * select one of the forms in `options` object which - * key matches exactly `value`. - * - * @example - * ``` - * import { select } from "@lingui/macro"; - * const message = select(gender, { - * male: "he", - * female: "she", - * other: "they", - * }); - * ``` - * - * @param value The key of choices to use - * @param choices - */ - export function select( - value: string, - choices: Record & BasicType - ): string - - /** - * Define a message for later use - * - * `defineMessage` can be used to add comments for translators, - * or to override the message ID. - * - * @example - * ``` - * import { defineMessage } from "@lingui/macro"; - * const message = defineMessage({ - * comment: "Greetings on the welcome page", - * message: `Welcome, ${name}!`, - * }); - * ``` - * - * @param descriptor The message descriptor - */ - export function defineMessage( - descriptor: MessageDescriptor - ): MessageDescriptor - - export type ChoiceProps = { - value?: string | number - } & ChoiceOptions - - /** - * The types should be changed after this PR is merged - * https://github.com/Microsoft/TypeScript/pull/26797 - * - * then we should be able to specify that key of values is same type as value. - * We would be able to remove separate type Values = {...} definition - * eg. - * type SelectProps = { - * value?: Values - * [key: Values]: string - * } - * - */ - type Values = { [key: string]: string } - - export type SelectProps = { - value: string - other: any - } & Values - - export const Trans: any - export const Plural: any - export const Select: any - export const SelectOrdinal: any -} diff --git a/packages/macro/index.d.ts b/packages/macro/index.d.ts index 805e960f2..5ba37c1c8 100644 --- a/packages/macro/index.d.ts +++ b/packages/macro/index.d.ts @@ -1,296 +1,12 @@ -import type { ReactElement, ReactNode, VFC, FC } from "react" -import type { I18n, MessageDescriptor } from "@lingui/core" -import type { TransRenderProps } from "@lingui/react" - -export type ChoiceOptions = { - /** Offset of value when calculating plural forms */ - offset?: number - zero?: string - one?: string - two?: string - few?: string - many?: string - - /** Catch-all option */ - other?: string - /** Exact match form, corresponds to =N rule */ - [digit: `${number}`]: string -} - -type MacroMessageDescriptor = ( - | { - id: string - message?: string - } - | { - id?: string - message: string - } -) & { - comment?: string - context?: string -} - -/** - * Translates a message descriptor - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * const message = t({ - * id: "msg.hello", - * comment: "Greetings at the homepage", - * message: `Hello ${name}`, - * }); - * ``` - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * const message = t({ - * id: "msg.plural", - * message: plural(value, { one: "...", other: "..." }), - * }); - * ``` - * - * @param descriptor The message descriptor to translate - */ -export function t(descriptor: MacroMessageDescriptor): string - -/** - * Translates a template string using the global I18n instance - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * const message = t`Hello ${name}`; - * ``` - */ -export function t( - literals: TemplateStringsArray, - ...placeholders: any[] -): string - -/** - * Translates a template string or message descriptor using a given I18n instance - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * import { I18n } from "@lingui/core"; - * const i18n = new I18n({ - * locale: "nl", - * messages: { "Hello {0}": "Hallo {0}" }, - * }); - * const message = t(i18n)`Hello ${name}`; - * ``` - * - * @example - * ``` - * import { t } from "@lingui/macro"; - * import { I18n } from "@lingui/core"; - * const i18n = new I18n({ - * locale: "nl", - * messages: { "Hello {0}": "Hallo {0}" }, - * }); - * const message = t(i18n)({ message: `Hello ${name}` }); - * ``` - */ -export function t(i18n: I18n): { - (literals: TemplateStringsArray, ...placeholders: any[]): string - (descriptor: MacroMessageDescriptor): string -} - -/** - * Pluralize a message - * - * @example - * ``` - * import { plural } from "@lingui/macro"; - * const message = plural(count, { - * one: "# Book", - * other: "# Books", - * }); - * ``` - * - * @param value Determines the plural form - * @param options Object with available plural forms - */ -export function plural(value: number | string, options: ChoiceOptions): string - -/** - * Pluralize a message using ordinal forms - * - * Similar to `plural` but instead of using cardinal plural forms, - * it uses ordinal forms. - * - * @example - * ``` - * import { selectOrdinal } from "@lingui/macro"; - * const message = selectOrdinal(count, { - * one: "#st", - * two: "#nd", - * few: "#rd", - * other: "#th", - * }); - * ``` - * - * @param value Determines the plural form - * @param options Object with available plural forms - */ -export function selectOrdinal( - value: number | string, - options: ChoiceOptions -): string - -type SelectOptions = { - /** Catch-all option */ - other: string - [matches: string]: string -} - -/** - * Selects a translation based on a value - * - * Select works like a switch statement. It will - * select one of the forms in `options` object which - * key matches exactly `value`. - * - * @example - * ``` - * import { select } from "@lingui/macro"; - * const message = select(gender, { - * male: "he", - * female: "she", - * other: "they", - * }); - * ``` - * - * @param value The key of choices to use - * @param choices - */ -export function select(value: string, choices: SelectOptions): string - -/** - * Define a message for later use - * - * `defineMessage` can be used to add comments for translators, - * or to override the message ID. - * - * @example - * ``` - * import { defineMessage } from "@lingui/macro"; - * const message = defineMessage({ - * comment: "Greetings on the welcome page", - * message: `Welcome, ${name}!`, - * }); - * ``` - * - * @param descriptor The message descriptor - */ -export function defineMessage( - descriptor: MacroMessageDescriptor -): MessageDescriptor - -type CommonProps = { - id?: string - comment?: string - context?: string - render?: (props: TransRenderProps) => ReactElement | null - i18n?: I18n -} - -type TransProps = { - children: ReactNode -} & CommonProps - -type PluralChoiceProps = { - value: string | number - /** Offset of value when calculating plural forms */ - offset?: number - zero?: ReactNode - one?: ReactNode - two?: ReactNode - few?: ReactNode - many?: ReactNode - - /** Catch-all option */ - other: ReactNode - /** Exact match form, corresponds to =N rule */ - [digit: `_${number}`]: ReactNode -} & CommonProps - -type SelectChoiceProps = { - value: string - /** Catch-all option */ - other: ReactNode - [option: `_${string}`]: ReactNode -} & CommonProps - -/** - * Trans is the basic macro for static messages, - * messages with variables, but also for messages with inline markup - * - * @example - * ``` - * Hello {username}. Read the docs. - * ``` - * @example - * ``` - * Hello {username}. - * ``` - */ -export const Trans: FC - -/** - * Props of Plural macro are transformed into plural format. - * - * @example - * ``` - * import { Plural } from "@lingui/macro" - * - * - * // ↓ ↓ ↓ ↓ ↓ ↓ - * import { Trans } from "@lingui/react" - * - * ``` - */ -export const Plural: VFC -/** - * Props of SelectOrdinal macro are transformed into selectOrdinal format. - * - * @example - * ``` - * // count == 1 -> 1st - * // count == 2 -> 2nd - * // count == 3 -> 3rd - * // count == 4 -> 4th - * - * ``` - */ -export const SelectOrdinal: VFC - -/** - * Props of Select macro are transformed into select format - * - * @example - * ``` - * // gender == "female" -> Her book - * // gender == "male" -> His book - * // gender == "non-binary" -> Their book - * - * - Message - -) - -// @ts-expect-error: `value` could be string only -m = - -// @ts-expect-error: `value` required -m = - -// @ts-expect-error: exact cases should be prefixed with underscore -m = ...} - other={...} - /> -) diff --git a/packages/macro/package.json b/packages/macro/package.json index f658d6d78..f6bca688e 100644 --- a/packages/macro/package.json +++ b/packages/macro/package.json @@ -33,15 +33,18 @@ ], "dependencies": { "@babel/runtime": "^7.20.13", - "@babel/types": "^7.20.7", - "@lingui/conf": "3.17.1", - "@lingui/cli": "3.17.1" + "@lingui/macro-lib": "^3.17.1" }, "peerDependencies": { - "@lingui/core": "^3.13.0", - "@lingui/react": "^3.13.0", + "@lingui/core": "^3.17.1", + "@lingui/react": "^3.17.1", "babel-plugin-macros": "2 || 3" }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + }, "devDependencies": { "@types/babel-plugin-macros": "^2.8.5", "tsd": "^0.25.0" diff --git a/packages/macro/src/index.ts b/packages/macro/src/index.ts index ca11cf22a..2b768cae2 100644 --- a/packages/macro/src/index.ts +++ b/packages/macro/src/index.ts @@ -1,173 +1,21 @@ import { createMacro, MacroParams } from "babel-plugin-macros" -import { getConfig as loadConfig, LinguiConfigNormalized } from "@lingui/conf" - -import MacroJS from "./macroJs" -import MacroJSX from "./macroJsx" -import { NodePath } from "@babel/traverse" -import { - ImportDeclaration, - isImportSpecifier, - isIdentifier, -} from "@babel/types" - -export type LinguiMacroOpts = { - // explicitly set by CLI when running extraction process - extract?: boolean - linguiConfig?: LinguiConfigNormalized -} - -const jsMacroTags = new Set([ - "defineMessage", - "arg", - "t", - "plural", - "select", - "selectOrdinal", -]) - -const jsxMacroTags = new Set(["Trans", "Plural", "Select", "SelectOrdinal"]) - -let config: LinguiConfigNormalized - -function getConfig(_config?: LinguiConfigNormalized) { - if (_config) { - config = _config - } - if (!config) { - config = loadConfig() - } - return config -} - -function macro({ references, state, babel, config }: MacroParams) { - const opts: LinguiMacroOpts = config as LinguiMacroOpts - - const { - i18nImportModule, - i18nImportName, - TransImportModule, - TransImportName, - } = getConfig(opts.linguiConfig).runtimeConfigModule - - const jsxNodes = new Set() - const jsNodes = new Set() - let needsI18nImport = false - - Object.keys(references).forEach((tagName) => { - const nodes = references[tagName] - - if (jsMacroTags.has(tagName)) { - nodes.forEach((node) => { - jsNodes.add(node.parentPath) - }) - } else if (jsxMacroTags.has(tagName)) { - // babel-plugin-macros return JSXIdentifier nodes. - // Which is for every JSX element would be presented twice (opening / close) - // Here we're taking JSXElement and dedupe it. - nodes.forEach((node) => { - // identifier.openingElement.jsxElement - jsxNodes.add(node.parentPath.parentPath) - }) - } else { - throw nodes[0].buildCodeFrameError(`Unknown macro ${tagName}`) - } - }) - - const stripNonEssentialProps = - process.env.NODE_ENV == "production" && !opts.extract - - const jsNodesArray = Array.from(jsNodes) - - jsNodesArray.filter(isRootPath(jsNodesArray)).forEach((path) => { - const macro = new MacroJS(babel, { i18nImportName, stripNonEssentialProps }) - if (macro.replacePath(path)) needsI18nImport = true - }) - - const jsxNodesArray = Array.from(jsxNodes) - - jsxNodesArray.filter(isRootPath(jsxNodesArray)).forEach((path) => { - const macro = new MacroJSX(babel, { stripNonEssentialProps }) - macro.replacePath(path) - }) - - if (needsI18nImport) { - addImport(babel, state, i18nImportModule, i18nImportName) - } - - if (jsxNodes.size) { - addImport(babel, state, TransImportModule, TransImportName) +import { jsMacroTags, macro as macroJs } from "@lingui/core/macro" +import { jsxMacroTags, macro as macroJsx } from "@lingui/react/macro" +import { throwNonMacroContextError } from "@lingui/macro-lib" + +export default createMacro( + (params: MacroParams) => { + macroJs(params) + macroJsx(params) + }, + { + configName: "lingui", } -} - -function addImport( - babel: MacroParams["babel"], - state: MacroParams["state"], - module: string, - importName: string -) { - const { types: t } = babel - - const linguiImport = state.file.path.node.body.find( - (importNode) => - t.isImportDeclaration(importNode) && - importNode.source.value === module && - // https://github.com/lingui/js-lingui/issues/777 - importNode.importKind !== "type" - ) as ImportDeclaration - - const tIdentifier = t.identifier(importName) - // Handle adding the import or altering the existing import - if (linguiImport) { - if ( - linguiImport.specifiers.findIndex( - (specifier) => - isImportSpecifier(specifier) && - isIdentifier(specifier.imported, { name: importName }) - ) === -1 - ) { - linguiImport.specifiers.push(t.importSpecifier(tIdentifier, tIdentifier)) - } - } else { - state.file.path.node.body.unshift( - t.importDeclaration( - [t.importSpecifier(tIdentifier, tIdentifier)], - t.stringLiteral(module) - ) - ) - } -} - -/** - * Filtering nested macro calls - * - * - * <-- this would be filtered out - * - */ -function isRootPath(allPath: NodePath[]) { - return (node: NodePath) => - (function traverse(path): boolean { - if (!path.parentPath) { - return true - } else { - return !allPath.includes(path.parentPath) && traverse(path.parentPath) - } - })(node) -} - -;[...jsMacroTags, ...jsxMacroTags].forEach((name) => { +) +;[...jsxMacroTags, ...jsMacroTags].forEach((name) => { Object.defineProperty(module.exports, name, { get() { - throw new Error( - `The macro you imported from "@lingui/macro" is being executed outside the context of compilation with babel-plugin-macros. ` + - `This indicates that you don't have the babel plugin "babel-plugin-macros" configured correctly. ` + - `Please see the documentation for how to configure babel-plugin-macros properly: ` + - "https://github.com/kentcdodds/babel-plugin-macros/blob/main/other/docs/user.md" - ) + throwNonMacroContextError("@lingui/macro") }, }) }) - -export default createMacro(macro, { - configName: "lingui", -}) diff --git a/packages/macro/src/utils.ts b/packages/macro/src/utils.ts deleted file mode 100644 index 17261d099..000000000 --- a/packages/macro/src/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const makeCounter = - (index = 0) => - () => - index++ diff --git a/packages/macro/test/index.ts b/packages/macro/test/index.ts index eab967d63..e057768dd 100644 --- a/packages/macro/test/index.ts +++ b/packages/macro/test/index.ts @@ -7,7 +7,7 @@ import { transformSync, } from "@babel/core" import prettier from "prettier" -import { LinguiMacroOpts } from "../src/index" +import type { LinguiMacroOpts } from "@lingui/macro-lib" import { JSXAttribute, jsxExpressionContainer, @@ -182,8 +182,8 @@ describe("macro", function () { it("Should throw error if used without babel-macro-plugin", async () => { await expect(async () => { - const mod = await import("../src/index") - return (mod as unknown as typeof import("@lingui/macro")).Trans + const mod = await import("@lingui/macro") + return mod.Trans }).rejects.toThrow('The macro you imported from "@lingui/macro"') }) diff --git a/packages/react/package.json b/packages/react/package.json index a197e9f53..1e2224a6c 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -42,6 +42,16 @@ "default": "./build/esm/index.js" } }, + "./macro": { + "require": { + "types": "./build/index.d.ts", + "default": "./build/cjs/index.js" + }, + "import": { + "types": "./build/index.d.ts", + "default": "./build/esm/index.js" + } + }, "./package.json": "./package.json" }, "files": [ @@ -50,13 +60,26 @@ "build/" ], "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "babel-plugin-macros": "2 || 3" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } }, "dependencies": { "@babel/runtime": "^7.20.13", - "@lingui/core": "3.17.1" + "@lingui/core": "3.17.1", + "@lingui/conf": "3.17.1", + "@lingui/macro-lib": "3.17.1", + "@lingui/cli": "3.17.1" }, "devDependencies": { - "react-testing-library": "^8.0.1" + "react-testing-library": "^8.0.1", + "@types/babel-plugin-macros": "^2.8.5", + "@babel/traverse":"^7.20.13", + "@babel/types": "^7.20.7", + "babel-plugin-macros": "^3.1.0" } } diff --git a/packages/react/src/macro/index.ts b/packages/react/src/macro/index.ts new file mode 100644 index 000000000..462ac30a6 --- /dev/null +++ b/packages/react/src/macro/index.ts @@ -0,0 +1,91 @@ +import { createMacro, MacroParams } from "babel-plugin-macros" +import { getConfig as loadConfig, LinguiConfigNormalized } from "@lingui/conf" + +import { MacroJSX } from "./macroJsx" +import type { NodePath } from "@babel/traverse" +import { + addImport, + isRootPath, + LinguiMacroOpts, + throwNonMacroContextError, +} from "@lingui/macro-lib" + +export const jsxMacroTags = new Set([ + "Trans", + "Plural", + "Select", + "SelectOrdinal", +]) + +let config: LinguiConfigNormalized + +// todo, make memoization on the conf library level +function getConfig(_config?: LinguiConfigNormalized) { + if (_config) { + config = _config + } + if (!config) { + config = loadConfig() + } + return config +} + +export function macro({ references, state, babel, config }: MacroParams) { + const opts: LinguiMacroOpts = config as LinguiMacroOpts + + const { TransImportModule, TransImportName } = getConfig( + opts.linguiConfig + ).runtimeConfigModule + + const jsxNodes = new Set() + + Object.keys(references).forEach((tagName) => { + const nodes = references[tagName] + + if (jsxMacroTags.has(tagName)) { + // babel-plugin-macros return JSXIdentifier nodes. + // Which is for every JSX element would be presented twice (opening / close) + // Here we're taking JSXElement and dedupe it. + nodes.forEach((node) => { + // identifier.openingElement.jsxElement + jsxNodes.add(node.parentPath.parentPath) + }) + } + // else { + // throw nodes[0].buildCodeFrameError(`Unknown macro ${tagName}`) + // } + }) + + const stripNonEssentialProps = + process.env.NODE_ENV == "production" && !opts.extract + + const jsxNodesArray = Array.from(jsxNodes) + + jsxNodesArray.filter(isRootPath(jsxNodesArray)).forEach((path) => { + const macro = new MacroJSX(babel, { stripNonEssentialProps }) + macro.replacePath(path) + }) + + if (jsxNodes.size) { + addImport( + babel.types, + state.file.path.node, + TransImportModule, + TransImportName + ) + } +} + +;[...jsxMacroTags].forEach((name) => { + Object.defineProperty(module.exports, name, { + get() { + throwNonMacroContextError("@lingui/react/macro") + }, + }) +}) + +export default createMacro(macro, { + configName: "lingui", +}) + +export * from "./types" diff --git a/packages/macro/src/macroJsx.test.ts b/packages/react/src/macro/macroJsx.test.ts similarity index 99% rename from packages/macro/src/macroJsx.test.ts rename to packages/react/src/macro/macroJsx.test.ts index 83da17c5f..a1bee50c5 100644 --- a/packages/macro/src/macroJsx.test.ts +++ b/packages/react/src/macro/macroJsx.test.ts @@ -1,5 +1,5 @@ import * as types from "@babel/types" -import MacroJSX, { normalizeWhitespace } from "./macroJsx" +import { normalizeWhitespace, MacroJSX } from "./macroJsx" import { transformSync } from "@babel/core" import type { NodePath } from "@babel/traverse" import type { JSXElement } from "@babel/types" diff --git a/packages/macro/src/macroJsx.ts b/packages/react/src/macro/macroJsx.ts similarity index 97% rename from packages/macro/src/macroJsx.ts rename to packages/react/src/macro/macroJsx.ts index f09f92ab1..fe671d3ba 100644 --- a/packages/macro/src/macroJsx.ts +++ b/packages/react/src/macro/macroJsx.ts @@ -1,5 +1,5 @@ -import * as babelTypes from "@babel/types" -import { +import type * as babelTypes from "@babel/types" +import type { ConditionalExpression, Expression, JSXAttribute, @@ -12,16 +12,20 @@ import { StringLiteral, TemplateLiteral, } from "@babel/types" -import { NodePath } from "@babel/traverse" +import type { NodePath } from "@babel/traverse" -import ICUMessageFormat, { +import { + makeCounter, + COMMENT, + CONTEXT, + ID, + MESSAGE, + ICUMessageFormat, ArgToken, ElementToken, TextToken, Token, -} from "./icu" -import { makeCounter } from "./utils" -import { COMMENT, CONTEXT, ID, MESSAGE } from "./constants" +} from "@lingui/macro-lib" import { generateMessageId } from "@lingui/cli/api" const pluralRuleRe = /(_[\d\w]+|zero|one|two|few|many|other)/ @@ -67,7 +71,7 @@ export type MacroJsxOpts = { stripNonEssentialProps: boolean } -export default class MacroJSX { +export class MacroJSX { types: typeof babelTypes expressionIndex = makeCounter() elementIndex = makeCounter() @@ -89,7 +93,7 @@ export default class MacroJSX { replacePath = (path: NodePath) => { const tokens = this.tokenizeNode(path) - const messageFormat = new ICUMessageFormat() + const messageFormat = new ICUMessageFormat(this.types) const { message: messageRaw, values, diff --git a/packages/react/src/macro/types.test-d.tsx b/packages/react/src/macro/types.test-d.tsx new file mode 100644 index 000000000..8e6941acb --- /dev/null +++ b/packages/react/src/macro/types.test-d.tsx @@ -0,0 +1,110 @@ +import { Trans, Plural, Select, SelectOrdinal } from "./types" +import React from "react" + +// @ts-expect-error: is never read +let m: any + +/////////////////// +//// JSX Trans +/////////////////// + +m = Message +m = ( + + Message + +) + +// @ts-expect-error: children are required here +m = + +/////////////////// +//// JSX Plural +/////////////////// +m = ( + // @ts-expect-error: children are not allowed + + Message + +) + +// @ts-expect-error: value is required +m = + +m = + +// @ts-expect-error: offset could be number only +m = + +// @ts-expect-error: not allowed prop is passed +m = + +// should support JSX element as Props +m = ...} other={...} /> + +// value as string +m = + +// @ts-expect-error: `other` should always be present +m = + +// additional properties +m = ( + +) + +/////////////////// +//// JSX SelectOrdinal is the same s Plural, so just smoke test it +/////////////////// +m = ( + +) + +/////////////////// +//// JSX Select +/////////////////// +const gender = "male" + +m = ( + // @ts-expect-error: children are not allowed here + +) + +// @ts-expect-error: `value` could be string only +m = + +// @ts-expect-error: `value` required +m = + +// @ts-expect-error: exact cases should be prefixed with underscore +m = ...} + other={...} + /> +) diff --git a/packages/react/src/macro/types.ts b/packages/react/src/macro/types.ts new file mode 100644 index 000000000..da592e247 --- /dev/null +++ b/packages/react/src/macro/types.ts @@ -0,0 +1,106 @@ +import type { ReactElement, ReactNode, VFC, FC } from "react" +import type { I18n } from "@lingui/core" +import type { TransRenderProps } from "@lingui/react" + +type CommonProps = { + id?: string + comment?: string + context?: string + render?: (props: TransRenderProps) => ReactElement | null + i18n?: I18n +} + +type TransProps = { + children: ReactNode +} & CommonProps + +type PluralChoiceProps = { + value: string | number + /** Offset of value when calculating plural forms */ + offset?: number + zero?: ReactNode + one?: ReactNode + two?: ReactNode + few?: ReactNode + many?: ReactNode + + /** Catch-all option */ + other: ReactNode + /** Exact match form, corresponds to =N rule */ + [digit: `_${number}`]: ReactNode +} & CommonProps + +type SelectChoiceProps = { + value: string + /** Catch-all option */ + other: ReactNode + [option: `_${string}`]: ReactNode +} & CommonProps + +/** + * Trans is the basic macro for static messages, + * messages with variables, but also for messages with inline markup + * + * @example + * ``` + * Hello {username}. Read the docs. + * ``` + * @example + * ``` + * Hello {username}. + * ``` + */ +export declare const Trans: FC + +/** + * Props of Plural macro are transformed into plural format. + * + * @example + * ``` + * import { Plural } from "@lingui/macro" + * + * + * // ↓ ↓ ↓ ↓ ↓ ↓ + * import { Trans } from "@lingui/react" + * + * ``` + */ +export declare const Plural: VFC +/** + * Props of SelectOrdinal macro are transformed into selectOrdinal format. + * + * @example + * ``` + * // count == 1 -> 1st + * // count == 2 -> 2nd + * // count == 3 -> 3rd + * // count == 4 -> 4th + * + * ``` + */ +export declare const SelectOrdinal: VFC + +/** + * Props of Select macro are transformed into select format + * + * @example + * ``` + * // gender == "female" -> Her book + * // gender == "male" -> His book + * // gender == "non-binary" -> Their book + * + *