Skip to content

Commit 2745dba

Browse files
committed
[INTERNAL] Validate variants and generated locales against BCP47
1 parent 31c02fd commit 2745dba

File tree

2 files changed

+83
-12
lines changed

2 files changed

+83
-12
lines changed

lib/processors/manifestEnhancer.js

+16-7
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ const APP_DESCRIPTOR_V22 = new Version("1.21.0");
1111
* Matches a legacy Java locale string, which is the format used by the UI5 Runtime (ResourceBundle)
1212
* to load i18n properties files.
1313
* Special case: "sr_Latn" is also supported, although the BCP47 script part is not supported by the Java locale format.
14+
*
15+
* Variants are limited to the format from BCP47, but with underscores instead of hyphens.
1416
*/
15-
// [ language ] [ region ] [ variants ]
16-
const rLegacyJavaLocale = /^([a-z]{2,3}|sr_Latn)(?:_([A-Z]{2}|\d{3})(?:_([a-zA-Z0-9]+))?)?$/;
17+
// [ language ] [ region ][ variants ]
18+
const rLegacyJavaLocale = /^([a-z]{2,3}|sr_Latn)(?:_([A-Z]{2}|\d{3})((?:_[0-9a-zA-Z]{5,8}|_[0-9][0-9a-zA-Z]{3})*)?)?$/;
1719

18-
const sapSupportabilityLocales = ["saptrc", "sappsd", "saprigi"];
20+
// See https://github.com/SAP/openui5/blob/a8f36e430f1fac172eb705811da4a2af25483408/src/sap.ui.core/src/sap/base/i18n/ResourceBundle.js#L76
21+
const sapSupportabilityVariants = ["saptrc", "sappsd", "saprigi"];
1922

2023
function getBCP47LocaleFromPropertiesFilename(locale) {
2124
const match = rLegacyJavaLocale.exec(locale);
@@ -25,15 +28,20 @@ function getBCP47LocaleFromPropertiesFilename(locale) {
2528
let [, language, region, variants] = match;
2629
let script;
2730

31+
variants = variants?.slice(1); // Remove leading underscore
32+
2833
// Special handling of sr_Latn (see regex above)
29-
// Note: This needs to be in sync with the runtime logic in sap/base/i18n/ResourceBundle
34+
// Note: This needs to be in sync with the runtime logic:
35+
// https://github.com/SAP/openui5/blob/a8f36e430f1fac172eb705811da4a2af25483408/src/sap.ui.core/src/sap/base/i18n/ResourceBundle.js#L202
3036
if (language === "sr_Latn") {
3137
language = "sr";
3238
script = "Latn";
3339
}
3440

35-
if (language === "en" && region === "US" && sapSupportabilityLocales.includes(variants)) {
36-
// Convert to private use section (aligned with ResourceBundle behavior)
41+
if (language === "en" && region === "US" && sapSupportabilityVariants.includes(variants)) {
42+
// Convert to private use section
43+
// Note: This needs to be in sync with the runtime logic:
44+
// https://github.com/SAP/openui5/blob/a8f36e430f1fac172eb705811da4a2af25483408/src/sap.ui.core/src/sap/base/i18n/ResourceBundle.js#L190
3745
variants = `x-${variants}`;
3846
}
3947

@@ -45,7 +53,8 @@ function getBCP47LocaleFromPropertiesFilename(locale) {
4553
bcp47Locale += `-${region}`;
4654
}
4755
if (variants) {
48-
bcp47Locale += `-${variants}`;
56+
// Convert to BCP47 variant format
57+
bcp47Locale += `-${variants.replace(/_/g, "-")}`;
4958
}
5059
return bcp47Locale;
5160
}

test/lib/processors/manifestEnhancer.js

+67-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,34 @@ import test from "ava";
22
import sinonGlobal from "sinon";
33
import esmock from "esmock";
44

5+
function isValidBCP47Locale(locale) {
6+
if (locale === "") {
7+
// Special handling of empty string, as this marks the developer locale (without locale information)
8+
return true;
9+
}
10+
11+
// See https://github.com/SAP/openui5/blob/a8f36e430f1fac172eb705811da4a2af25483408/src/sap.ui.core/src/sap/base/i18n/ResourceBundle.js#L30
12+
/**
13+
* A regular expression that describes language tags according to BCP-47.
14+
*
15+
* @see BCP47 "Tags for Identifying Languages" (http://www.ietf.org/rfc/bcp/bcp47.txt)
16+
*
17+
* The matching groups are
18+
* 0=all
19+
* 1=language (shortest ISO639 code + ext. language sub tags | 4digits (reserved) | registered language sub tags)
20+
* 2=script (4 letters)
21+
* 3=region (2letter language or 3 digits)
22+
* 4=variants (separated by '-', Note: capturing group contains leading '-' to shorten the regex!)
23+
* 5=extensions (including leading singleton, multiple extensions separated by '-')
24+
* 6=private use section (including leading 'x', multiple sections separated by '-')
25+
*/
26+
27+
// eslint-disable-next-line max-len
28+
// [-------------------- language ----------------------][--- script ---][------- region --------][------------- variants --------------][----------- extensions ------------][------ private use -------]
29+
const rLocale = /^((?:[A-Z]{2,3}(?:-[A-Z]{3}){0,3})|[A-Z]{4}|[A-Z]{5,8})(?:-([A-Z]{4}))?(?:-([A-Z]{2}|[0-9]{3}))?((?:-[0-9A-Z]{5,8}|-[0-9][0-9A-Z]{3})*)((?:-[0-9A-WYZ](?:-[0-9A-Z]{2,8})+)*)(?:-(X(?:-[0-9A-Z]{1,8})+))?$/i;
30+
return rLocale.test(locale);
31+
}
32+
533
test.beforeEach(async (t) => {
634
const sinon = t.context.sinon = sinonGlobal.createSandbox();
735
t.context.logWarnSpy = sinon.spy();
@@ -3033,24 +3061,32 @@ test("manifestEnhancer#getSupportedLocales", async (t) => {
30333061
.callsArgWith(1, null, [
30343062
"i18n.properties",
30353063
"i18n_ar_001.properties",
3064+
"i18n_ar_001_variant.properties",
30363065
"i18n_crn.properties",
30373066
"i18n_en.properties",
30383067
"i18n_en_US.properties",
30393068
"i18n_en_US_saptrc.properties",
3040-
"i18n_sr_Latn_RS.properties"
3069+
"i18n_en_US_saprigi.properties",
3070+
"i18n_sr_Latn_RS.properties",
3071+
"i18n_sr_Latn_RS_variant.properties"
30413072
]);
30423073

30433074
const expectedLocales = [
30443075
"",
30453076
"ar-001",
3077+
"ar-001-variant",
30463078
"crn",
30473079
"en",
30483080
"en-US",
3081+
"en-US-x-saprigi",
30493082
"en-US-x-saptrc",
3050-
"sr-Latn-RS"
3083+
"sr-Latn-RS",
3084+
"sr-Latn-RS-variant",
30513085
];
30523086

3053-
t.deepEqual(await manifestEnhancer.getSupportedLocales("./i18n/i18n.properties"), expectedLocales);
3087+
const generatedLocales = await manifestEnhancer.getSupportedLocales("./i18n/i18n.properties");
3088+
3089+
t.deepEqual(generatedLocales, expectedLocales);
30543090
t.deepEqual(await manifestEnhancer.getSupportedLocales("i18n/../i18n/i18n.properties"), expectedLocales);
30553091
t.deepEqual(await manifestEnhancer.getSupportedLocales("ui5://sap/ui/demo/app/i18n/i18n.properties"), expectedLocales);
30563092

@@ -3066,6 +3102,12 @@ test("manifestEnhancer#getSupportedLocales", async (t) => {
30663102
t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged");
30673103
t.true(t.context.logWarnSpy.notCalled, "No warnings should be logged");
30683104
t.true(t.context.logErrorSpy.notCalled, "No errors should be logged");
3105+
3106+
// Check whether generated locales are valid BCP47 locales, as the UI5 runtime
3107+
// fails if a locale is not a valid
3108+
generatedLocales.forEach((locale) => {
3109+
t.true(isValidBCP47Locale(locale), `Generated locale '${locale}' should be a valid BCP47 locale`);
3110+
});
30693111
});
30703112

30713113
test("manifestEnhancer#getSupportedLocales (invalid file names)", async (t) => {
@@ -3102,7 +3144,19 @@ test("manifestEnhancer#getSupportedLocales (invalid file names)", async (t) => {
31023144
"i18n_sr_Latn_RS_variant_x_private.properties",
31033145

31043146
// Invalid: Legacy Java locale format does have BCP47 "extension" / "private use" sections
3105-
"i18n_sr_Latn_RS_variant_f_11_x_private.properties"
3147+
"i18n_sr_Latn_RS_variant_f_11_x_private.properties",
3148+
3149+
// Invalid: Invalid variant length (too short)
3150+
"i18n_de_CH_var.properties",
3151+
3152+
// Invalid: Invalid variant length (too long)
3153+
"i18n_de_CH_variant11.properties",
3154+
3155+
// Invalid: Invalid variant length (too long)
3156+
"i18n_de_CH_001FOOBAR.properties",
3157+
3158+
// Invalid: Should be "en_US_saprigi"
3159+
"i18n_en_US_x_saprigi.properties"
31063160
];
31073161

31083162
fs.readdir.withArgs("/i18n")
@@ -3117,7 +3171,7 @@ test("manifestEnhancer#getSupportedLocales (invalid file names)", async (t) => {
31173171

31183172
t.is(fs.readdir.callCount, 1);
31193173

3120-
t.is(t.context.logWarnSpy.callCount, 6);
3174+
t.is(t.context.logWarnSpy.callCount, 10);
31213175
t.is(t.context.logWarnSpy.getCall(0).args[0],
31223176
"Skipping invalid file 'i18n_en-US.properties' for bundle 'i18n/i18n.properties'");
31233177
t.is(t.context.logWarnSpy.getCall(1).args[0],
@@ -3130,6 +3184,14 @@ test("manifestEnhancer#getSupportedLocales (invalid file names)", async (t) => {
31303184
"Skipping invalid file 'i18n_sr_Latn_RS_variant_x_private.properties' for bundle 'i18n/i18n.properties'");
31313185
t.is(t.context.logWarnSpy.getCall(5).args[0],
31323186
"Skipping invalid file 'i18n_sr_Latn_RS_variant_f_11_x_private.properties' for bundle 'i18n/i18n.properties'");
3187+
t.is(t.context.logWarnSpy.getCall(6).args[0],
3188+
"Skipping invalid file 'i18n_de_CH_var.properties' for bundle 'i18n/i18n.properties'");
3189+
t.is(t.context.logWarnSpy.getCall(7).args[0],
3190+
"Skipping invalid file 'i18n_de_CH_variant11.properties' for bundle 'i18n/i18n.properties'");
3191+
t.is(t.context.logWarnSpy.getCall(8).args[0],
3192+
"Skipping invalid file 'i18n_de_CH_001FOOBAR.properties' for bundle 'i18n/i18n.properties'");
3193+
t.is(t.context.logWarnSpy.getCall(9).args[0],
3194+
"Skipping invalid file 'i18n_en_US_x_saprigi.properties' for bundle 'i18n/i18n.properties'");
31333195
t.true(t.context.logVerboseSpy.notCalled, "No verbose messages should be logged");
31343196
t.true(t.context.logErrorSpy.notCalled, "No errors should be logged");
31353197
});

0 commit comments

Comments
 (0)