feat(): Introduce translation module and preliminary application of them (#14189)

* feat(): Translation first steps

* feat(): locale middleware

* feat(): readonly links

* feat(): feature flag

* feat(): modules sdk

* feat(): translation module re export

* start adding workflows

* update typings

* update typings

* test(): Add integration tests

* test(): centralize filters preparation

* test(): centralize filters preparation

* remove unnecessary importy

* fix workflows

* Define StoreLocale inside Store Module

* Link definition to extend Store with supported_locales

* store_locale migration

* Add supported_locales handling in Store Module

* Tests

* Accept supported_locales in Store endpoints

* Add locales to js-sdk

* Include locale list and default locale in Store Detail section

* Initialize local namespace in js-sdk

* Add locales route

* Make code primary key of locale table to facilitate upserts

* Add locales routes

* Show locale code as is

* Add list translations api route

* Batch endpoint

* Types

* New batchTranslationsWorkflow and various updates to existent ones

* Edit default locale UI

* WIP

* Apply translation agnostically

* middleware

* Apply translation agnostically

* fix Apply translation agnostically

* apply translations to product list

* Add feature flag

* fetch translations by batches of 250 max

* fix apply

* improve and test util

* apply to product list

* dont manage translations if no locale

* normalize locale

* potential todo

* Protect translations routes with feature flag

* Extract normalize locale util to core/utils

* Normalize locale on write

* Normalize locale for read

* Use feature flag to guard translations UI across the board

* Avoid throwing incorrectly when locale_code not present in partial updates

* move applyTranslations util

* remove old tests

* fix util tests

* fix(): product end points

* cleanup

* update lock

* remove unused var

* cleanup

* fix apply locale

* missing new dep for test utils

* Change entity_type, entity_id to reference, reference_id

* Remove comment

* Avoid registering translations route if ff not enabled

* Prevent registering express handler for disabled route via defineFileConfig

* Add tests

* Add changeset

* Update test

* fix integration tests, module and internals

* Add locale id plus fixed

* Allow to pass array of reference_id

* fix unit tests

* fix link loading

* fix store route

* fix sales channel test

* fix tests

---------

Co-authored-by: Nicolas Gorga <nicogorga11@gmail.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-12-08 19:33:08 +01:00
committed by GitHub
parent fea3d4ec49
commit 6dc0b8bed8
130 changed files with 5649 additions and 112 deletions

View File

@@ -0,0 +1,172 @@
import { normalizeLocale } from "../normalize-locale"
describe("normalizeLocale", function () {
it("should normalize single segment locales to lowercase", function () {
const expectations = [
{
input: "eN",
output: "en",
},
{
input: "EN",
output: "en",
},
{
input: "En",
output: "en",
},
{
input: "en",
output: "en",
},
{
input: "fr",
output: "fr",
},
{
input: "FR",
output: "fr",
},
{
input: "de",
output: "de",
},
]
expectations.forEach((expectation) => {
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
})
})
it("should normalize two segment locales (language-region)", function () {
const expectations = [
{
input: "en-Us",
output: "en-US",
},
{
input: "EN-US",
output: "en-US",
},
{
input: "en-us",
output: "en-US",
},
{
input: "En-Us",
output: "en-US",
},
{
input: "fr-FR",
output: "fr-FR",
},
{
input: "FR-fr",
output: "fr-FR",
},
{
input: "de-DE",
output: "de-DE",
},
{
input: "es-ES",
output: "es-ES",
},
{
input: "pt-BR",
output: "pt-BR",
},
]
expectations.forEach((expectation) => {
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
})
})
it("should normalize three segment locales (language-script-region)", function () {
const expectations = [
{
input: "RU-cYrl-By",
output: "ru-Cyrl-BY",
},
{
input: "ru-cyrl-by",
output: "ru-Cyrl-BY",
},
{
input: "RU-CYRL-BY",
output: "ru-Cyrl-BY",
},
{
input: "zh-Hans-CN",
output: "zh-Hans-CN",
},
{
input: "ZH-HANS-CN",
output: "zh-Hans-CN",
},
{
input: "sr-Latn-RS",
output: "sr-Latn-RS",
},
{
input: "SR-LATN-RS",
output: "sr-Latn-RS",
},
]
expectations.forEach((expectation) => {
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
})
})
it("should return locale as-is for more than three segments", function () {
const expectations = [
{
input: "en-US-x-private",
output: "en-US-x-private",
},
{
input: "en-US-x-private-extended",
output: "en-US-x-private-extended",
},
{
input: "en-US-x-private-extended-more",
output: "en-US-x-private-extended-more",
},
]
expectations.forEach((expectation) => {
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
})
})
it("should handle edge cases", function () {
const expectations = [
{
input: "",
output: "",
},
{
input: "a",
output: "a",
},
{
input: "A",
output: "a",
},
{
input: "a-B",
output: "a-B",
},
{
input: "a-b-C",
output: "a-B-C",
},
]
expectations.forEach((expectation) => {
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
})
})
})

View File

@@ -185,6 +185,9 @@ function resolveModules(
{ resolve: MODULE_PACKAGE_NAMES[Modules.ORDER] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.SETTINGS] },
// TODO: re-enable this once we have the final release
// { resolve: MODULE_PACKAGE_NAMES[Modules.TRANSLATION] },
{
resolve: MODULE_PACKAGE_NAMES[Modules.AUTH],
options: {

View File

@@ -53,6 +53,7 @@ export * from "./medusa-container"
export * from "./merge-metadata"
export * from "./merge-plugin-modules"
export * from "./normalize-csv-value"
export * from "./normalize-locale"
export * from "./normalize-import-path-with-source"
export * from "./object-from-string-path"
export * from "./object-to-string-path"

View File

@@ -0,0 +1,40 @@
import { upperCaseFirst } from "./upper-case-first"
/**
* Normalizes a locale string to {@link https://developer.mozilla.org/en-US/docs/Glossary/BCP_47_language_tag|BCP 47 language tag format}
* @param locale - The locale string to normalize
* @returns The normalized locale string
*
* @example
* input: "en-Us"
* output: "en-US"
*
* @example
* input: "eN"
* output: "en"
*
* @example
* input: "RU-cYrl-By"
* output: "ru-Cyrl-BY"
*/
export function normalizeLocale(locale: string) {
const segments = locale.split("-")
if (segments.length === 1) {
return segments[0].toLowerCase()
}
// e.g en-US
if (segments.length === 2) {
return `${segments[0].toLowerCase()}-${segments[1].toUpperCase()}`
}
// e.g ru-Cyrl-BY
if (segments.length === 3) {
return `${segments[0].toLowerCase()}-${upperCaseFirst(
segments[1].toLowerCase()
)}-${segments[2].toUpperCase()}`
}
return locale
}