Almost done, left to extract the extra language resources
This commit is contained in:
parent
bb163132fe
commit
b0b6b994ed
@ -10,12 +10,26 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
import * as fs from "fs";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
|
||||
export type BuildContextLike = {
|
||||
themeNames: string[];
|
||||
themeSrcDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function generateMessageProperties(params: {
|
||||
themeSrcDirPath: string;
|
||||
buildContext: BuildContextLike;
|
||||
themeType: ThemeType;
|
||||
}): { languageTag: string; propertiesFileSource: string }[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
}): {
|
||||
languageTags: string[];
|
||||
writeMessagePropertiesFiles: (params: {
|
||||
messageDirPath: string;
|
||||
themeName: string;
|
||||
}) => void;
|
||||
} {
|
||||
const { buildContext, themeType } = params;
|
||||
|
||||
const baseMessagesDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
@ -25,8 +39,9 @@ export function generateMessageProperties(params: {
|
||||
"messages_defaultSet"
|
||||
);
|
||||
|
||||
const baseMessageBundle: { [languageTag: string]: Record<string, string> } =
|
||||
Object.fromEntries(
|
||||
const messages_defaultSet_by_languageTag_defaultSet: {
|
||||
[languageTag_defaultSet: string]: Record<string, string>;
|
||||
} = Object.fromEntries(
|
||||
fs
|
||||
.readdirSync(baseMessagesDirPath)
|
||||
.filter(basename => basename !== "index.ts" && basename !== "types.ts")
|
||||
@ -35,10 +50,7 @@ export function generateMessageProperties(params: {
|
||||
filePath: pathJoin(baseMessagesDirPath, basename)
|
||||
}))
|
||||
.map(({ languageTag, filePath }) => {
|
||||
const lines = fs
|
||||
.readFileSync(filePath)
|
||||
.toString("utf8")
|
||||
.split(/\r?\n/);
|
||||
const lines = fs.readFileSync(filePath).toString("utf8").split(/\r?\n/);
|
||||
|
||||
let messagesJson = "{";
|
||||
|
||||
@ -69,7 +81,7 @@ export function generateMessageProperties(params: {
|
||||
|
||||
const { i18nTsFilePath } = (() => {
|
||||
let files = crawl({
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
dirPath: pathJoin(buildContext.themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
});
|
||||
|
||||
@ -88,7 +100,7 @@ export function generateMessageProperties(params: {
|
||||
files = files.sort((a, b) => a.length - b.length);
|
||||
|
||||
files = files.filter(file =>
|
||||
fs.readFileSync(file).toString("utf8").includes("createUseI18n(")
|
||||
fs.readFileSync(file).toString("utf8").includes("i18nBuilder")
|
||||
);
|
||||
|
||||
const i18nTsFilePath: string | undefined = files[0];
|
||||
@ -96,13 +108,13 @@ export function generateMessageProperties(params: {
|
||||
return { i18nTsFilePath };
|
||||
})();
|
||||
|
||||
const messageBundle: { [languageTag: string]: Record<string, string> } | undefined =
|
||||
(() => {
|
||||
const i18nTsRoot = (() => {
|
||||
if (i18nTsFilePath === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const root = recast.parse(fs.readFileSync(i18nTsFilePath).toString("utf8"), {
|
||||
const root: recast.types.ASTNode = recast.parse(
|
||||
fs.readFileSync(i18nTsFilePath).toString("utf8"),
|
||||
{
|
||||
parser: {
|
||||
parse: (code: string) =>
|
||||
babelParser.parse(code, {
|
||||
@ -112,19 +124,30 @@ export function generateMessageProperties(params: {
|
||||
generator: babelGenerate,
|
||||
types: babelTypes
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
return root;
|
||||
})();
|
||||
|
||||
let messageBundleDeclarationTsCode: string | undefined = undefined;
|
||||
const messages_defaultSet_by_languageTag_notInDefaultSet:
|
||||
| { [languageTag_notInDefaultSet: string]: Record<string, string> }
|
||||
| undefined = (() => {
|
||||
if (i18nTsRoot === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
recast.visit(root, {
|
||||
let firstArgumentCode: string | undefined = undefined;
|
||||
|
||||
recast.visit(i18nTsRoot, {
|
||||
visitCallExpression: function (path) {
|
||||
const node = path.node;
|
||||
|
||||
if (
|
||||
path.node.callee.type === "Identifier" &&
|
||||
path.node.callee.name === "createUseI18n"
|
||||
node.callee.type === "MemberExpression" &&
|
||||
node.callee.property.type === "Identifier" &&
|
||||
node.callee.property.name === "withExtraLanguages"
|
||||
) {
|
||||
messageBundleDeclarationTsCode = babelGenerate(
|
||||
path.node.arguments[0] as any
|
||||
).code;
|
||||
firstArgumentCode = babelGenerate(node.arguments[0] as any).code;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -132,15 +155,62 @@ export function generateMessageProperties(params: {
|
||||
}
|
||||
});
|
||||
|
||||
assert(messageBundleDeclarationTsCode !== undefined);
|
||||
if (firstArgumentCode === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let messageBundle: {
|
||||
[languageTag: string]: Record<string, string>;
|
||||
//todo
|
||||
|
||||
//TODO
|
||||
return {};
|
||||
})();
|
||||
|
||||
const messages_defaultSet_by_languageTag = {
|
||||
...messages_defaultSet_by_languageTag_defaultSet,
|
||||
...messages_defaultSet_by_languageTag_notInDefaultSet
|
||||
};
|
||||
|
||||
const messages_themeDefined_by_languageTag:
|
||||
| {
|
||||
[languageTag: string]:
|
||||
| Record<string, string | Record<string, string>>
|
||||
| undefined;
|
||||
}
|
||||
| undefined = (() => {
|
||||
if (i18nTsRoot === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let firstArgumentCode: string | undefined = undefined;
|
||||
|
||||
recast.visit(i18nTsRoot, {
|
||||
visitCallExpression: function (path) {
|
||||
const node = path.node;
|
||||
|
||||
if (
|
||||
node.callee.type === "MemberExpression" &&
|
||||
node.callee.property.type === "Identifier" &&
|
||||
node.callee.property.name === "withCustomTranslations"
|
||||
) {
|
||||
firstArgumentCode = babelGenerate(node.arguments[0] as any).code;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.traverse(path);
|
||||
}
|
||||
});
|
||||
|
||||
if (firstArgumentCode === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let messages_themeDefined_by_languageTag: {
|
||||
[languageTag: string]: Record<string, string | Record<string, string>>;
|
||||
} = {};
|
||||
|
||||
try {
|
||||
eval(
|
||||
`${symToStr({ messageBundle })} = ${messageBundleDeclarationTsCode}`
|
||||
`${symToStr({ messages_themeDefined_by_languageTag })} = ${firstArgumentCode}`
|
||||
);
|
||||
} catch {
|
||||
console.warn(
|
||||
@ -151,42 +221,79 @@ export function generateMessageProperties(params: {
|
||||
"\n",
|
||||
"The following code could not be evaluated:",
|
||||
"\n",
|
||||
messageBundleDeclarationTsCode
|
||||
firstArgumentCode
|
||||
].join(" ")
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return messageBundle;
|
||||
return messages_themeDefined_by_languageTag;
|
||||
})();
|
||||
|
||||
const mergedMessageBundle: { [languageTag: string]: Record<string, string> } =
|
||||
Object.fromEntries(
|
||||
Object.entries(baseMessageBundle).map(([languageTag, messages]) => [
|
||||
languageTag,
|
||||
{
|
||||
...messages,
|
||||
...(messageBundle === undefined
|
||||
? {}
|
||||
: messageBundle[languageTag] ??
|
||||
messageBundle[FALLBACK_LANGUAGE_TAG] ??
|
||||
messageBundle[Object.keys(messageBundle)[0]] ??
|
||||
{})
|
||||
}
|
||||
])
|
||||
);
|
||||
const languageTags = Object.keys(messages_defaultSet_by_languageTag);
|
||||
|
||||
const messageProperties: { languageTag: string; propertiesFileSource: string }[] =
|
||||
Object.entries(mergedMessageBundle).map(([languageTag, messages]) => ({
|
||||
languageTag,
|
||||
propertiesFileSource: [
|
||||
return {
|
||||
languageTags,
|
||||
writeMessagePropertiesFiles: ({ messageDirPath, themeName }) => {
|
||||
for (const languageTag of languageTags) {
|
||||
const messages = {
|
||||
...messages_defaultSet_by_languageTag[languageTag]
|
||||
};
|
||||
|
||||
add_theme_defined_messages: {
|
||||
if (messages_themeDefined_by_languageTag === undefined) {
|
||||
break add_theme_defined_messages;
|
||||
}
|
||||
|
||||
let messages_themeDefined =
|
||||
messages_themeDefined_by_languageTag[languageTag];
|
||||
|
||||
if (messages_themeDefined === undefined) {
|
||||
messages_themeDefined =
|
||||
messages_themeDefined_by_languageTag[FALLBACK_LANGUAGE_TAG];
|
||||
}
|
||||
if (messages_themeDefined === undefined) {
|
||||
messages_themeDefined =
|
||||
messages_themeDefined_by_languageTag[
|
||||
Object.keys(messages_themeDefined_by_languageTag)[0]
|
||||
];
|
||||
}
|
||||
if (messages_themeDefined === undefined) {
|
||||
break add_theme_defined_messages;
|
||||
}
|
||||
|
||||
for (const [key, messageOrMessageByThemeName] of Object.entries(
|
||||
messages_themeDefined
|
||||
)) {
|
||||
const message = (() => {
|
||||
if (typeof messageOrMessageByThemeName === "string") {
|
||||
return messageOrMessageByThemeName;
|
||||
}
|
||||
|
||||
const message = messageOrMessageByThemeName[themeName];
|
||||
|
||||
assert(message !== undefined);
|
||||
|
||||
return message;
|
||||
})();
|
||||
|
||||
messages[key] = message;
|
||||
}
|
||||
}
|
||||
|
||||
const propertiesFileSource = [
|
||||
"",
|
||||
...(themeType !== "account" ? ["parent=base"] : []),
|
||||
...Object.entries(messages).map(
|
||||
([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
|
||||
),
|
||||
""
|
||||
].join("\n")
|
||||
}));
|
||||
].join("\n");
|
||||
|
||||
return messageProperties;
|
||||
fs.writeFileSync(
|
||||
pathJoin(messageDirPath, `messages_${languageTag}.properties`),
|
||||
Buffer.from(propertiesFileSource, "utf8")
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export async function generateResources(params: {
|
||||
rmSync(resourcesDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await generateResourcesForMainTheme({
|
||||
const { writeMessagePropertiesFiles } = await generateResourcesForMainTheme({
|
||||
resourcesDirPath,
|
||||
themeName,
|
||||
buildContext
|
||||
@ -36,7 +36,8 @@ export async function generateResources(params: {
|
||||
generateResourcesForThemeVariant({
|
||||
resourcesDirPath,
|
||||
themeName,
|
||||
themeVariantName
|
||||
themeVariantName,
|
||||
writeMessagePropertiesFiles
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,10 @@ import type { BuildContext } from "../../shared/buildContext";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||
import { readExtraPagesNames } from "./readExtraPageNames";
|
||||
import { generateMessageProperties } from "./generateMessageProperties";
|
||||
import {
|
||||
generateMessageProperties,
|
||||
type BuildContextLike as BuildContextLike_generateMessageProperties
|
||||
} from "./generateMessageProperties";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||
import {
|
||||
@ -29,7 +32,8 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
|
||||
import * as child_process from "child_process";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & {
|
||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||
BuildContextLike_generateMessageProperties & {
|
||||
extraThemeProperties: string[] | undefined;
|
||||
projectDirPath: string;
|
||||
projectBuildDirPath: string;
|
||||
@ -38,15 +42,20 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & {
|
||||
themeSrcDirPath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function generateResourcesForMainTheme(params: {
|
||||
buildContext: BuildContextLike;
|
||||
themeName: string;
|
||||
resourcesDirPath: string;
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<void> {
|
||||
}): Promise<{
|
||||
writeMessagePropertiesFiles: (params: {
|
||||
getMessageDirPath: (params: { themeType: ThemeType }) => string;
|
||||
themeName: string;
|
||||
}) => void;
|
||||
}> {
|
||||
const { themeName, resourcesDirPath, buildContext } = params;
|
||||
|
||||
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
|
||||
@ -54,6 +63,10 @@ export async function generateResourcesForMainTheme(params: {
|
||||
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
|
||||
};
|
||||
|
||||
const writeMessagePropertiesFilesByThemeType: Partial<
|
||||
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
||||
> = {};
|
||||
|
||||
for (const themeType of ["login", "account"] as const) {
|
||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
||||
continue;
|
||||
@ -187,30 +200,27 @@ export async function generateResourcesForMainTheme(params: {
|
||||
);
|
||||
});
|
||||
|
||||
let languageTags: string[] | undefined = undefined;
|
||||
|
||||
i18n_messages_generation: {
|
||||
if (isForAccountSpa) {
|
||||
break i18n_messages_generation;
|
||||
}
|
||||
|
||||
generateMessageProperties({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
const wrap = generateMessageProperties({
|
||||
buildContext,
|
||||
themeType
|
||||
}).forEach(({ languageTag, propertiesFileSource }) => {
|
||||
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
|
||||
|
||||
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
const propertiesFilePath = pathJoin(
|
||||
messagesDirPath,
|
||||
`messages_${languageTag}.properties`
|
||||
);
|
||||
languageTags = wrap.languageTags;
|
||||
const { writeMessagePropertiesFiles } = wrap;
|
||||
|
||||
fs.writeFileSync(
|
||||
propertiesFilePath,
|
||||
Buffer.from(propertiesFileSource, "utf8")
|
||||
);
|
||||
writeMessagePropertiesFilesByThemeType[themeType] =
|
||||
writeMessagePropertiesFiles;
|
||||
|
||||
writeMessagePropertiesFiles({
|
||||
messageDirPath: pathJoin(themeTypeDirPath, "messages"),
|
||||
themeName
|
||||
});
|
||||
}
|
||||
|
||||
@ -280,7 +290,10 @@ export async function generateResourcesForMainTheme(params: {
|
||||
...(buildContext.extraThemeProperties ?? []),
|
||||
...buildContext.environmentVariables.map(
|
||||
({ name, default: defaultValue }) =>
|
||||
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
||||
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`,
|
||||
...(languageTags === undefined
|
||||
? []
|
||||
: `locales=${languageTags.join(",")}`)
|
||||
)
|
||||
].join("\n\n"),
|
||||
"utf8"
|
||||
@ -338,4 +351,20 @@ export async function generateResourcesForMainTheme(params: {
|
||||
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
writeMessagePropertiesFiles: ({ getMessageDirPath, themeName }) => {
|
||||
objectEntries(writeMessagePropertiesFilesByThemeType).forEach(
|
||||
([themeType, writeMessagePropertiesFiles]) => {
|
||||
if (writeMessagePropertiesFiles === undefined) {
|
||||
return;
|
||||
}
|
||||
writeMessagePropertiesFiles({
|
||||
messageDirPath: getMessageDirPath({ themeType }),
|
||||
themeName
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,27 +1,27 @@
|
||||
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export type BuildContextLike = {
|
||||
keycloakifyBuildDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
|
||||
export function generateResourcesForThemeVariant(params: {
|
||||
resourcesDirPath: string;
|
||||
themeName: string;
|
||||
themeVariantName: string;
|
||||
writeMessagePropertiesFiles: (params: {
|
||||
getMessageDirPath: (params: { themeType: ThemeType }) => string;
|
||||
themeName: string;
|
||||
}) => void;
|
||||
}) {
|
||||
const { resourcesDirPath, themeName, themeVariantName } = params;
|
||||
const { resourcesDirPath, themeName, themeVariantName, writeMessagePropertiesFiles } =
|
||||
params;
|
||||
|
||||
const mainThemeDirPath = pathJoin(resourcesDirPath, "theme", themeName);
|
||||
const themeVariantDirPath = pathJoin(mainThemeDirPath, "..", themeVariantName);
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: mainThemeDirPath,
|
||||
destDirPath: pathJoin(mainThemeDirPath, "..", themeVariantName),
|
||||
destDirPath: themeVariantDirPath,
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (
|
||||
pathExtname(fileRelativePath) === ".ftl" &&
|
||||
@ -67,4 +67,10 @@ export function generateResourcesForThemeVariant(params: {
|
||||
return newMetaInfKeycloakTheme;
|
||||
}
|
||||
});
|
||||
|
||||
writeMessagePropertiesFiles({
|
||||
getMessageDirPath: ({ themeType }) =>
|
||||
pathJoin(themeVariantDirPath, themeType, "messages"),
|
||||
themeName: themeVariantName
|
||||
});
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ export function createGetI18n<
|
||||
|
||||
let label = labelBySupportedLanguageTag[languageTag];
|
||||
|
||||
if (label === undefined) {
|
||||
if (label === undefined || label === "" || label === languageTag) {
|
||||
assert(is<Exclude<LanguageTag, LanguageTag_defaultSet>>(languageTag));
|
||||
|
||||
const entry = extraLanguageTranslations[languageTag];
|
||||
|
94
stories/login/gpt.md
Normal file
94
stories/login/gpt.md
Normal file
@ -0,0 +1,94 @@
|
||||
Hello GPT,
|
||||
|
||||
So, I'm using recast in a node script to parse a typescript source file and extract the part that I'm intrested in.
|
||||
|
||||
Example of the source file:
|
||||
|
||||
```ts
|
||||
import { createUseI18n } from "keycloakify/login";
|
||||
|
||||
export const { useI18n, ofTypeI18n } = createUseI18n({
|
||||
en: {
|
||||
myCustomMessage: "My custom message"
|
||||
},
|
||||
fr: {
|
||||
myCustomMessage: "Mon message personnalisé"
|
||||
}
|
||||
});
|
||||
|
||||
export type I18n = typeof ofTypeI18n;
|
||||
```
|
||||
|
||||
The string that I want to extract from this source file is:
|
||||
|
||||
```raw
|
||||
{
|
||||
en: {
|
||||
myCustomMessage: "My custom message"
|
||||
},
|
||||
fr: {
|
||||
myCustomMessage: "Mon message personnalisé"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is my script:
|
||||
|
||||
```ts
|
||||
const root = recast.parse(fs.readFileSync(i18nTsFilePath).toString("utf8"), {
|
||||
parser: {
|
||||
parse: (code: string) =>
|
||||
babelParser.parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript"]
|
||||
}),
|
||||
generator: babelGenerate,
|
||||
types: babelTypes
|
||||
}
|
||||
});
|
||||
|
||||
let messageBundleDeclarationTsCode: string | undefined = undefined;
|
||||
|
||||
recast.visit(root, {
|
||||
visitCallExpression: function (path) {
|
||||
if (
|
||||
path.node.callee.type === "Identifier" &&
|
||||
path.node.callee.name === "createUseI18n"
|
||||
) {
|
||||
messageBundleDeclarationTsCode = babelGenerate(
|
||||
path.node.arguments[0] as any
|
||||
).code;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.traverse(path);
|
||||
}
|
||||
});
|
||||
|
||||
// Here messageBundleDeclarationTsCode contains the string I want
|
||||
```
|
||||
|
||||
It works, but now, the API has changed. The source file looks like this:
|
||||
|
||||
```ts
|
||||
import { i18nBuilder } from "keycloakify/login/i18n";
|
||||
|
||||
const { useI18n, ofTypeI18n } = i18nBuilder
|
||||
.withThemeName<"my-theme-1" | "my-theme-2">()
|
||||
.withCustomTranslations({
|
||||
en: {
|
||||
myCustomMessage: "My custom message"
|
||||
},
|
||||
fr: {
|
||||
myCustomMessage: "Mon message personnalisé"
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
type I18n = typeof ofTypeI18n;
|
||||
|
||||
export { useI18n, type I18n };
|
||||
```
|
||||
|
||||
Can you modify the script to extract the string taking into account the change that have been made to the source file?
|
||||
(I need to extract the argument that is passed to the `withCustomTranslations` method)
|
Loading…
x
Reference in New Issue
Block a user