feat(admin-*,dashboard): add dashboard i18n extensions (#13763)
* virtual i18n module
* changeset
* fallback ns
fallback to the default "translation" ns if the key isnt found. Allows to use a single "useTranslation("customNs")" hook for both custom and medusa-provided keys
* simplify merges
* optional for backward compat
* fix HMR
* fix generated deepMerge
* test
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
|
||||
import * as utils from "../../utils"
|
||||
import { generateI18n } from "../generate-i18n"
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock("../../utils", async () => {
|
||||
const actual = await vi.importActual("../../utils")
|
||||
return {
|
||||
...actual,
|
||||
crawl: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
const expectedI18nSingleSource = `
|
||||
resources: i18nTranslations0
|
||||
`
|
||||
|
||||
const expectedI18nMultipleSources = `
|
||||
resources: deepMerge(deepMerge(i18nTranslations0, i18nTranslations1), i18nTranslations2)
|
||||
`
|
||||
|
||||
const expectedI18nNoSources = `
|
||||
resources: {}
|
||||
`
|
||||
|
||||
describe("generateI18n", () => {
|
||||
it("should generate i18n with single source", async () => {
|
||||
const mockFiles = ["Users/user/medusa/src/admin/i18n/index.ts"]
|
||||
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||
|
||||
const result = await generateI18n(
|
||||
new Set(["Users/user/medusa/src/admin"])
|
||||
)
|
||||
|
||||
expect(result.imports).toEqual([
|
||||
`import i18nTranslations0 from "Users/user/medusa/src/admin/i18n/index.ts"`,
|
||||
])
|
||||
expect(utils.normalizeString(result.code)).toEqual(
|
||||
utils.normalizeString(expectedI18nSingleSource)
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle windows paths", async () => {
|
||||
const mockFiles = ["C:\\medusa\\src\\admin\\i18n\\index.ts"]
|
||||
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||
|
||||
const result = await generateI18n(new Set(["C:\\medusa\\src\\admin"]))
|
||||
|
||||
expect(result.imports).toEqual([
|
||||
`import i18nTranslations0 from "C:/medusa/src/admin/i18n/index.ts"`,
|
||||
])
|
||||
expect(utils.normalizeString(result.code)).toEqual(
|
||||
utils.normalizeString(expectedI18nSingleSource)
|
||||
)
|
||||
})
|
||||
|
||||
it("should generate i18n with multiple sources", async () => {
|
||||
vi.mocked(utils.crawl)
|
||||
.mockResolvedValueOnce(["Users/user/medusa/src/admin/i18n/index.ts"])
|
||||
.mockResolvedValueOnce(["Users/user/medusa/src/plugin1/i18n/index.ts"])
|
||||
.mockResolvedValueOnce(["Users/user/medusa/src/plugin2/i18n/index.ts"])
|
||||
|
||||
const result = await generateI18n(
|
||||
new Set([
|
||||
"Users/user/medusa/src/admin",
|
||||
"Users/user/medusa/src/plugin1",
|
||||
"Users/user/medusa/src/plugin2",
|
||||
])
|
||||
)
|
||||
|
||||
expect(result.imports).toEqual([
|
||||
`import i18nTranslations0 from "Users/user/medusa/src/admin/i18n/index.ts"`,
|
||||
`import i18nTranslations1 from "Users/user/medusa/src/plugin1/i18n/index.ts"`,
|
||||
`import i18nTranslations2 from "Users/user/medusa/src/plugin2/i18n/index.ts"`,
|
||||
])
|
||||
expect(utils.normalizeString(result.code)).toEqual(
|
||||
utils.normalizeString(expectedI18nMultipleSources)
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle no i18n sources", async () => {
|
||||
vi.mocked(utils.crawl).mockResolvedValue([])
|
||||
|
||||
const result = await generateI18n(
|
||||
new Set(["Users/user/medusa/src/admin"])
|
||||
)
|
||||
|
||||
expect(result.imports).toEqual([])
|
||||
expect(utils.normalizeString(result.code)).toEqual(
|
||||
utils.normalizeString(expectedI18nNoSources)
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import fs from "fs/promises"
|
||||
import { generateHash } from "../utils"
|
||||
import { getI18nIndexFilesFromSources } from "./helpers"
|
||||
|
||||
export async function generateI18nHash(sources: Set<string>): Promise<string> {
|
||||
const indexFiles = await getI18nIndexFilesFromSources(sources)
|
||||
|
||||
const contents = await Promise.all(
|
||||
indexFiles.map(file => fs.readFile(file, "utf-8"))
|
||||
)
|
||||
|
||||
const totalContent = contents.join("")
|
||||
|
||||
return generateHash(totalContent)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { outdent } from "outdent"
|
||||
import { normalizePath } from "../utils"
|
||||
import { getI18nIndexFilesFromSources } from "./helpers"
|
||||
|
||||
export async function generateI18n(sources: Set<string>) {
|
||||
const indexFiles = await getI18nIndexFilesFromSources(sources)
|
||||
|
||||
const imports = indexFiles.map((file, index) => {
|
||||
const normalizedPath = normalizePath(file)
|
||||
return `import i18nTranslations${index} from "${normalizedPath}"`
|
||||
})
|
||||
|
||||
let mergeCode = '{}'
|
||||
if (indexFiles.length === 1) {
|
||||
mergeCode = 'i18nTranslations0'
|
||||
} else if (indexFiles.length > 1) {
|
||||
// Only happens in dev mode if there are 2+ plugins linked with plugin:develop
|
||||
// Chain deepMerge calls since it only accepts 2 arguments
|
||||
mergeCode = indexFiles.slice(1).reduce((acc, _, index) => {
|
||||
return `deepMerge(${acc}, i18nTranslations${index + 1})`
|
||||
}, 'i18nTranslations0')
|
||||
}
|
||||
|
||||
const code = outdent`
|
||||
resources: ${mergeCode}
|
||||
`
|
||||
|
||||
return {
|
||||
imports,
|
||||
code,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { crawl } from "../utils"
|
||||
|
||||
/**
|
||||
* Get i18n index files from sources
|
||||
* Looks for src/admin/i18n/index.ts in each source
|
||||
*/
|
||||
export async function getI18nIndexFilesFromSources(
|
||||
sources: Set<string>
|
||||
): Promise<string[]> {
|
||||
return (await Promise.all(
|
||||
Array.from(sources).map(async (source) =>
|
||||
crawl(`${source}/i18n`, "index", { min: 0, max: 0 })
|
||||
)
|
||||
)).flat()
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { generateI18nHash } from "./generate-i18n-hash"
|
||||
export { getI18nIndexFilesFromSources } from "./helpers"
|
||||
export { generateI18n } from "./generate-i18n"
|
||||
Reference in New Issue
Block a user