Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/lib/HAXCMS.js
Original file line number Diff line number Diff line change
Expand Up @@ -3229,6 +3229,15 @@ class HAXCMSClass {
if (!this.config.security.loginRateLimit) {
this.config.security.loginRateLimit = {};
}
// localization settings (parity with PHP HAXCMSLocalizationSettingsService):
// stored as a localization block on config.json so it is readable at boot
// before _config/settings/ is scanned. defaultLanguage is a BCP-47 tag.
if (!this.config.localization) {
this.config.localization = {};
}
if (!this.config.localization.defaultLanguage) {
this.config.localization.defaultLanguage = 'en-US';
}
// load in core theme data
let themeData = JSON.parse(fs.readFileSync(path.join(this.coreConfigPath, "themes.json"),
{encoding:'utf8', flag:'r'}, 'utf8'));
Expand Down
20 changes: 20 additions & 0 deletions src/lib/SystemRoutesMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,25 @@ addRouteHandler(
'configuration/media',
settingsRoutes.configurationMedia,
);

addRouteHandler(
SystemRoutesMap,
'get',
'configuration/localization',
settingsRoutes.configurationLocalization,
);
addRouteHandler(
SystemRoutesMap,
'post',
'configuration/localization',
settingsRoutes.configurationLocalization,
);
addRouteHandler(
SystemRoutesMap,
'patch',
'configuration/localization',
settingsRoutes.configurationLocalization,
);
addRouteHandler(
SystemRoutesMap,
'post',
Expand Down Expand Up @@ -315,6 +334,7 @@ const SystemV1AdminRoutes = [
'schemas',
'configuration/api-keys',
'configuration/media',
'configuration/localization',
'configuration/schema-files/operations',
'themes',
'blocks',
Expand Down
152 changes: 152 additions & 0 deletions src/lib/localizationSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
const fs = require('fs-extra');
const path = require('path');

const DEFAULT_LANGUAGE = 'en-US';
const BCP47_LANGUAGE_REGEX = /^[a-zA-Z]{2,3}(-[a-zA-Z]{2,4})?$/;
const DEFAULT_LOCALIZATION_SETTINGS = {
Comment on lines +4 to +6
defaultLanguage: DEFAULT_LANGUAGE,
};

function getLocalizationSettingsFilePath(haxcms) {
const configDirectory = (
haxcms &&
typeof haxcms.configDirectory === 'string' &&
haxcms.configDirectory
) ? haxcms.configDirectory : path.join(process.cwd(), '_config');
return path.join(configDirectory, 'config.json');
}

function normalizeDefaultLanguage(value) {
if (value === null || typeof value === 'undefined' || value === '') {
return null;
}
if (typeof value !== 'string') {
value = String(value);
}
value = value.trim();
if (value === '') {
return null;
}
if (!BCP47_LANGUAGE_REGEX.test(value)) {
return null;
}
const parts = value.split('-');
const primary = parts[0].toLowerCase();
if (parts.length > 1 && parts[1] !== '') {
const region = parts[1].toUpperCase();
return primary + '-' + region;
}
return primary;
}

function normalizeLocalizationSettings(input = {}) {
const source = (
input &&
typeof input === 'object' &&
!Array.isArray(input)
) ? input : {};
return {
defaultLanguage: normalizeDefaultLanguage(source.defaultLanguage),
};
}

function getEffectiveLocalizationSettings(settings = {}) {
const source = (
settings &&
typeof settings === 'object' &&
!Array.isArray(settings)
) ? settings : {};
return {
defaultLanguage: source.defaultLanguage == null ? DEFAULT_LANGUAGE : source.defaultLanguage,
};
}

function hasSupportedLocalizationSettingsPayload(input = {}) {
const source = (
input &&
typeof input === 'object' &&
!Array.isArray(input)
) ? input : {};
return Object.prototype.hasOwnProperty.call(source, 'defaultLanguage');
}

function isValidDefaultLanguagePayloadValue(value) {
if (value === null || typeof value === 'undefined' || value === '') {
return true;
}
return normalizeDefaultLanguage(value) !== null;
}

async function readLocalizationSettings(haxcms) {
const filePath = getLocalizationSettingsFilePath(haxcms);
let localizationBlock = {};
if (await fs.pathExists(filePath)) {
try {
const fullConfig = JSON.parse(await fs.readFile(filePath, 'utf8'));
if (
fullConfig &&
typeof fullConfig === 'object' &&
!Array.isArray(fullConfig) &&
fullConfig.localization
) {
localizationBlock = fullConfig.localization;
}
}
catch (e) {
localizationBlock = {};
}
}
return normalizeLocalizationSettings(localizationBlock);
}

async function writeLocalizationSettings(haxcms, settings = {}) {
const filePath = getLocalizationSettingsFilePath(haxcms);
const source = (
settings &&
typeof settings === 'object' &&
!Array.isArray(settings)
) ? settings : {};
let fullConfig = {};
if (await fs.pathExists(filePath)) {
try {
fullConfig = JSON.parse(await fs.readFile(filePath, 'utf8'));
}
catch (e) {
fullConfig = {};
}
}
if (!fullConfig || typeof fullConfig !== 'object' || Array.isArray(fullConfig)) {
fullConfig = {};
}
const existingLocalization = (
fullConfig.localization &&
typeof fullConfig.localization === 'object' &&
!Array.isArray(fullConfig.localization)
) ? fullConfig.localization : {};
const nextSettings = normalizeLocalizationSettings(existingLocalization);
if (Object.prototype.hasOwnProperty.call(source, 'defaultLanguage')) {
nextSettings.defaultLanguage = normalizeDefaultLanguage(source.defaultLanguage);
}
fullConfig.localization = nextSettings;
await fs.writeFile(
filePath,
`${JSON.stringify(fullConfig, null, 2)}\n`,
'utf8',
);
if (haxcms && haxcms.config && typeof haxcms.config === 'object') {
haxcms.config.localization = nextSettings;
}
return nextSettings;
}

module.exports = {
getLocalizationSettingsFilePath,
normalizeDefaultLanguage,
normalizeLocalizationSettings,
getEffectiveLocalizationSettings,
hasSupportedLocalizationSettingsPayload,
isValidDefaultLanguagePayloadValue,
DEFAULT_LOCALIZATION_SETTINGS,
readLocalizationSettings,
writeLocalizationSettings,
};
98 changes: 98 additions & 0 deletions src/openapi/system-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,87 @@ paths:
additionalProperties: true
"403":
$ref: "#/components/responses/Forbidden"
/system/api/v1/configuration/localization:
get:
tags:
- settings
operationId: getLocalizationSettings
summary: Return localization configuration
security:
- bearerAuth: []
userTokenHeader: []
responses:
"200":
description: Localization settings
content:
application/json:
schema:
type: object
properties:
status:
type: integer
data:
$ref: "#/components/schemas/LocalizationSettingsData"
additionalProperties: true
"403":
$ref: "#/components/responses/Forbidden"
post:
tags:
- settings
operationId: getLocalizationSettingsPost
summary: Return localization configuration (read alias of GET)
description: >
Read-only alias of GET /configuration/localization. POST is accepted for
callers that prefer POST over GET but does not write; use PATCH to
update localization settings. Single-user deployment assumption: the
NodeJS backend does not model an admin/superUser tier; these settings
are written by the single authenticated dashboard user.
security:
- bearerAuth: []
userTokenHeader: []
responses:
"200":
description: Localization settings
content:
application/json:
schema:
$ref: "#/components/schemas/ApiEnvelope"
"403":
$ref: "#/components/responses/Forbidden"
patch:
tags:
- settings
operationId: saveLocalizationSettingsPatch
summary: Update localization configuration
description: >
Write operation — updates localization configuration (defaultLanguage,
a BCP-47 tag). Single-user deployment assumption: the NodeJS backend
does not model an admin/superUser tier; settings are written by the
single authenticated dashboard user.
security:
- bearerAuth: []
userTokenHeader: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LocalizationSettings"
responses:
"200":
description: Localization settings update response
content:
application/json:
schema:
type: object
properties:
status:
type: integer
data:
$ref: "#/components/schemas/LocalizationSettingsData"
additionalProperties: true
"403":
$ref: "#/components/responses/Forbidden"
/system/api/v1/configuration/schema-files/operations:
post:
tags:
Expand Down Expand Up @@ -3004,6 +3085,23 @@ components:
acceptedFormats:
type: string
additionalProperties: true
LocalizationSettings:
type: object
description: Localization settings payload (defaultLanguage BCP-47 tag)
properties:
defaultLanguage:
type: string
description: BCP-47 language tag (e.g. en-US, es-ES, fr-FR)
additionalProperties: true
LocalizationSettingsData:
type: object
description: >
Localization settings returned by getLocalizationSettings and
saveLocalizationSettingsPatch. The front-end reads defaultLanguage.
properties:
defaultLanguage:
type: string
additionalProperties: true
Comment on lines +3088 to +3104
SchemaFileOperationData:
type: object
description: >
Expand Down
3 changes: 2 additions & 1 deletion src/systemRoutes/v1/routes/createSite.js
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,8 @@ async function createSite(req, res) {
schema.metadata.site.settings = {};
}
if (!schema.metadata.site.settings.lang) {
schema.metadata.site.settings.lang = 'en-US';
var systemDefaultLang = (HAXCMS.config.localization && HAXCMS.config.localization.defaultLanguage) || 'en-US';
schema.metadata.site.settings.lang = systemDefaultLang;
}
if (typeof schema.metadata.site.settings.publishPagesOn === 'undefined') {
schema.metadata.site.settings.publishPagesOn = true;
Expand Down
35 changes: 35 additions & 0 deletions src/systemRoutes/v1/routes/getLocalizationSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { HAXCMS } = require('../../../lib/HAXCMS.js');
const {
readLocalizationSettings,
getEffectiveLocalizationSettings,
} = require('../../../lib/localizationSettings.js');

/**
* @OA\Post(
* path="/getLocalizationSettings",
* tags={"cms","authenticated","settings"},
* @OA\Response(
* response="200",
* description="Load saved localization settings"
* )
* )
*/
async function getLocalizationSettings(req, res) {
try {
const localizationSettings = await readLocalizationSettings(HAXCMS);
return res.json({
status: 200,
data: getEffectiveLocalizationSettings(localizationSettings),
});
}
catch (e) {
return res.status(500).json({
status: 500,
data: {
message: 'Unable to load localization settings',
},
});
}
}

module.exports = getLocalizationSettings;
Loading
Loading