@red-hat-developer-hub/backstage-plugin-translations-backend 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-translations-backend
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e515331: Serve the maintained JSON translations from the RHDH repository's `/translations` directory and merge them with mounted customer overrides in the NFS translation API. Expose the backend `i18n.overrides` configuration schema.
8
+
3
9
  ## 0.5.1
4
10
 
5
11
  ## 0.5.0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Welcome to the translation backend plugin!
4
4
 
5
- This plugin provides a simple backend service in RHDH that serves translation files by returning their JSON content. It exposes an endpoint (/api/translation) and reads the specified JSON translation files.
5
+ This plugin serves JSON translation files from the RHDH repository's `/translations` directory and files mounted in the RHDH pod. It exposes `/api/translations`. Paths listed in `i18n.overrides` take highest priority. The result is cached until the backend restarts.
6
6
 
7
7
  ## Installation
8
8
 
@@ -26,7 +26,55 @@ backend.add(
26
26
  ## Getting started
27
27
 
28
28
  Your plugin has been added to the backend app in this repository, meaning you'll be able to access it by running `yarn
29
- start-backend` in the root directory, and then navigating to [/api/translation](http://localhost:7007/api/translation).
29
+ start-backend` in the root directory, and then navigating to [/api/translations](http://localhost:7007/api/translations).
30
+
31
+ ## Configuring JSON overrides
32
+
33
+ The backend discovers valid JSON files in the repository-root `/translations`
34
+ directory, including files mounted there. Shipped catalogs are merged in source
35
+ order, followed by customer files with other basenames. Paths in
36
+ `i18n.overrides` are applied after all discovered files, in the order listed;
37
+ use this setting when a file must take priority over another discovered file.
38
+
39
+ ```yaml title="app-config.yaml"
40
+ i18n:
41
+ overrides:
42
+ - /opt/app-root/src/translations/custom-overrides.json
43
+ ```
44
+
45
+ To add an override:
46
+
47
+ 1. Create a JSON file whose top-level key is a translation reference ID, then
48
+ add the locale and messages you want to override. Message keys use the same
49
+ names as the translation reference.
50
+
51
+ ```json
52
+ {
53
+ "plugin.homepage": {
54
+ "en": {
55
+ "header.welcome": "Welcome back! (custom override)"
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ 2. Place the file in the RHDH repository-root `translations/` directory for
62
+ local development, or mount it at `/opt/app-root/src/translations/` in a
63
+ deployment. Files in that directory are discovered automatically.
64
+
65
+ 3. Add the file's path to `i18n.overrides` when it must override values from
66
+ other discovered files. Use the path as seen by the backend process; for a
67
+ ConfigMap mounted at the path above, configure
68
+ `/opt/app-root/src/translations/custom-overrides.json`.
69
+
70
+ 4. Restart the backend after changing the file. The translation response is
71
+ cached until the backend restarts.
72
+
73
+ The repository demo config points to an explicit override in
74
+ `translations/app-config-overrides-demo.json`; its value takes precedence over
75
+ the automatically discovered demo file with the same key. In the local RHDH
76
+ workspace, the backend runs from `packages/backend`, so the configured path to
77
+ the root `translations/` directory starts with `../../translations/`.
30
78
 
31
79
  ## Development
32
80
 
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "i18n": {
6
+ "type": "object",
7
+ "properties": {
8
+ "overrides": {
9
+ "type": "array",
10
+ "items": {
11
+ "type": "string"
12
+ },
13
+ "description": "JSON files that override translations discovered in the `/translations` directory. Later files override earlier files.",
14
+ "visibility": "backend"
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
@@ -14,15 +14,11 @@ class TranslationService {
14
14
  this.config = config;
15
15
  this.logger = logger;
16
16
  this.overridesFiles = this.config.getOptionalStringArray("i18n.overrides") ?? [];
17
- this.configuredLocales = this.config.getOptionalStringArray(
18
- "i18n.locales"
19
- ) ?? ["en"];
20
17
  }
21
18
  config;
22
19
  logger;
23
20
  cachedTranslations = null;
24
21
  overridesFiles;
25
- configuredLocales;
26
22
  /**
27
23
  * Finds the topmost folder with the given name by searching up the directory tree
28
24
  * Similar to the approach used in extensions plugin
@@ -70,6 +66,25 @@ class TranslationService {
70
66
  );
71
67
  return null;
72
68
  }
69
+ /**
70
+ * Orders catalog files so higher-priority sources override earlier sources.
71
+ */
72
+ orderCatalogFiles(files) {
73
+ const sourceOrder = [
74
+ "backstage-",
75
+ "community-plugins-",
76
+ "rhdh-plugins-",
77
+ "rhdh-"
78
+ ];
79
+ const priority = (file) => {
80
+ const name = path__default.default.basename(file);
81
+ const index = sourceOrder.findIndex((prefix) => name.startsWith(prefix));
82
+ return index === -1 ? sourceOrder.length : index;
83
+ };
84
+ return files.sort(
85
+ (a, b) => priority(a) - priority(b) || a.localeCompare(b)
86
+ );
87
+ }
73
88
  /**
74
89
  * Scans a directory for JSON translation files
75
90
  */
@@ -152,7 +167,9 @@ class TranslationService {
152
167
  let internalFiles = [];
153
168
  const internalDirectory = this.resolveInternalTranslationsDirectory();
154
169
  if (internalDirectory) {
155
- internalFiles = this.scanDirectoryForTranslationFiles(internalDirectory);
170
+ internalFiles = this.orderCatalogFiles(
171
+ this.scanDirectoryForTranslationFiles(internalDirectory)
172
+ );
156
173
  if (internalFiles.length > 0) {
157
174
  const internalStats = this.processTranslationFiles(
158
175
  internalFiles,
@@ -199,10 +216,7 @@ class TranslationService {
199
216
  );
200
217
  throw new Error("No valid translation files found in the provided files");
201
218
  }
202
- this.cachedTranslations = utils.filterLocales(
203
- mergedTranslations,
204
- this.configuredLocales
205
- );
219
+ this.cachedTranslations = mergedTranslations;
206
220
  this.logger.info(
207
221
  `Translation processing complete: ${totalFilesProcessed} files processed, ${totalFilesNotFound} files not found, ${totalFilesInvalid} files invalid`
208
222
  );
@@ -1 +1 @@
1
- {"version":3,"file":"TranslationService.cjs.js","sources":["../../src/service/TranslationService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport fs from 'fs';\nimport path from 'path';\n\nimport {\n deepMergeTranslations,\n filterLocales,\n isValidJSONTranslation,\n} from '../utils';\n\nexport interface TranslationFileStats {\n filesProcessed: number;\n filesNotFound: number;\n filesInvalid: number;\n}\n\nexport interface TranslationResult {\n translations: Record<string, any>;\n stats: {\n totalFilesProcessed: number;\n totalFilesNotFound: number;\n totalFilesInvalid: number;\n };\n}\n\n/**\n * Service for managing translation files and processing\n */\nexport class TranslationService {\n private cachedTranslations: Record<string, any> | null = null;\n private readonly overridesFiles: string[];\n private readonly configuredLocales: string[];\n\n constructor(\n private readonly config: Config,\n private readonly logger: LoggerService,\n ) {\n this.overridesFiles =\n this.config.getOptionalStringArray('i18n.overrides') ?? [];\n this.configuredLocales = this.config.getOptionalStringArray(\n 'i18n.locales',\n ) ?? ['en'];\n }\n\n /**\n * Finds the topmost folder with the given name by searching up the directory tree\n * Similar to the approach used in extensions plugin\n */\n private findTopmostFolder(\n folderName: string,\n startPath = process.cwd(),\n ): string | null {\n let currentPath = path.resolve(startPath);\n let topmostFoundPath: string | null = null;\n\n while (currentPath !== path.parse(currentPath).root) {\n const targetFolderPath = path.join(currentPath, folderName);\n if (\n fs.existsSync(targetFolderPath) &&\n fs.statSync(targetFolderPath).isDirectory()\n ) {\n topmostFoundPath = targetFolderPath;\n }\n currentPath = path.dirname(currentPath);\n }\n const targetFolderPath = path.join(currentPath, folderName);\n\n if (\n fs.existsSync(targetFolderPath) &&\n fs.statSync(targetFolderPath).isDirectory()\n ) {\n topmostFoundPath = targetFolderPath;\n }\n\n return topmostFoundPath;\n }\n\n /**\n * Resolves the internal translations directory path\n * Uses dynamic folder search to find the topmost 'translations' folder\n */\n private resolveInternalTranslationsDirectory(): string | null {\n // First try to find 'src/translations' in the topmost directory structure\n const srcTranslationsPath = this.findTopmostFolder('src');\n if (srcTranslationsPath) {\n const translationsPath = path.join(srcTranslationsPath, 'translations');\n if (\n fs.existsSync(translationsPath) &&\n fs.statSync(translationsPath).isDirectory()\n ) {\n this.logger.info(\n `Found internal translations directory: ${translationsPath}`,\n );\n return translationsPath;\n }\n }\n\n // Fallback: look for 'translations' folder directly\n const translationsPath = this.findTopmostFolder('translations');\n if (translationsPath) {\n this.logger.info(\n `Found internal translations directory: ${translationsPath}`,\n );\n return translationsPath;\n }\n\n this.logger.debug(\n 'No internal translations directory found in any expected location',\n );\n return null;\n }\n\n /**\n * Scans a directory for JSON translation files\n */\n private scanDirectoryForTranslationFiles(directoryPath: string): string[] {\n try {\n if (!fs.existsSync(directoryPath)) {\n this.logger.warn(\n `Internal translation directory not found: ${directoryPath}`,\n );\n return [];\n }\n\n const files = fs.readdirSync(directoryPath);\n const jsonFiles = files\n .filter(file => file.endsWith('.json'))\n .map(file => path.join(directoryPath, file));\n\n if (jsonFiles.length > 0) {\n this.logger.info(\n `Found ${jsonFiles.length} translation files in internal directory: ${directoryPath}`,\n );\n }\n\n return jsonFiles;\n } catch (error) {\n this.logger.warn(\n `Failed to scan internal directory ${directoryPath}: ${error}`,\n );\n return [];\n }\n }\n\n /**\n * Processes translation files and merges them into the target object\n */\n private processTranslationFiles(\n files: string[],\n mergedTranslations: Record<string, any>,\n source: string,\n ): TranslationFileStats {\n let filesProcessed = 0;\n let filesNotFound = 0;\n let filesInvalid = 0;\n\n for (const file of files) {\n try {\n if (!fs.existsSync(file)) {\n this.logger.warn(`Translation file not found: ${file} (${source})`);\n filesNotFound++;\n continue;\n }\n\n const raw = fs.readFileSync(file, 'utf8');\n const json = JSON.parse(raw);\n if (!isValidJSONTranslation(json)) {\n this.logger.warn(\n `Invalid JSON translation file format: ${file} (${source})`,\n );\n filesInvalid++;\n continue;\n }\n\n deepMergeTranslations(mergedTranslations, json);\n this.logger.debug(`Processed translation file: ${file} (${source})`);\n filesProcessed++;\n } catch (parseError) {\n this.logger.warn(\n `Failed to parse JSON from file: ${file} (${source}) - ${parseError}`,\n );\n filesInvalid++;\n }\n }\n\n return { filesProcessed, filesNotFound, filesInvalid };\n }\n\n /**\n * Gets all translations, processing both internal directory and config overrides\n */\n async getTranslations(): Promise<TranslationResult> {\n if (this.cachedTranslations) {\n return {\n translations: this.cachedTranslations,\n stats: {\n totalFilesProcessed: 0,\n totalFilesNotFound: 0,\n totalFilesInvalid: 0,\n },\n };\n }\n\n const mergedTranslations: Record<string, any> = {};\n let totalFilesProcessed = 0;\n let totalFilesNotFound = 0;\n let totalFilesInvalid = 0;\n\n // Step 1: Process internal directory files first (auto-detected)\n let internalFiles: string[] = [];\n const internalDirectory = this.resolveInternalTranslationsDirectory();\n if (internalDirectory) {\n internalFiles = this.scanDirectoryForTranslationFiles(internalDirectory);\n if (internalFiles.length > 0) {\n const internalStats = this.processTranslationFiles(\n internalFiles,\n mergedTranslations,\n 'internal directory',\n );\n totalFilesProcessed += internalStats.filesProcessed;\n totalFilesNotFound += internalStats.filesNotFound;\n totalFilesInvalid += internalStats.filesInvalid;\n }\n }\n\n // Step 2: Process config override files (these will override internal files)\n if (this.overridesFiles.length > 0) {\n const overrideStats = this.processTranslationFiles(\n this.overridesFiles,\n mergedTranslations,\n 'config overrides',\n );\n totalFilesProcessed += overrideStats.filesProcessed;\n totalFilesNotFound += overrideStats.filesNotFound;\n totalFilesInvalid += overrideStats.filesInvalid;\n }\n\n // Determine if we have any files configured\n const totalFilesConfigured =\n internalFiles.length + this.overridesFiles.length;\n\n // If no files were configured, return empty response\n if (totalFilesConfigured === 0) {\n this.logger.info(\n 'No translation files found (neither auto-detected internal directory nor config overrides)',\n );\n this.cachedTranslations = {};\n return {\n translations: {},\n stats: {\n totalFilesProcessed: 0,\n totalFilesNotFound: 0,\n totalFilesInvalid: 0,\n },\n };\n }\n\n // If files were configured but none were found, throw error\n if (totalFilesNotFound === totalFilesConfigured) {\n this.logger.warn('All configured translation files were not found');\n throw new Error('All configured translation files were not found');\n }\n\n // If files were found but none contained valid translations, throw error\n if (totalFilesProcessed === 0) {\n this.logger.warn(\n 'No valid translation files found in the provided files',\n );\n throw new Error('No valid translation files found in the provided files');\n }\n\n // Filter translations by configured locales\n this.cachedTranslations = filterLocales(\n mergedTranslations,\n this.configuredLocales,\n );\n\n // Log summary\n this.logger.info(\n `Translation processing complete: ${totalFilesProcessed} files processed, ${totalFilesNotFound} files not found, ${totalFilesInvalid} files invalid`,\n );\n\n return {\n translations: this.cachedTranslations,\n stats: {\n totalFilesProcessed,\n totalFilesNotFound,\n totalFilesInvalid,\n },\n };\n }\n\n /**\n * Clears the cached translations (useful for testing or when files change)\n */\n clearCache(): void {\n this.cachedTranslations = null;\n }\n}\n"],"names":["path","targetFolderPath","fs","translationsPath","isValidJSONTranslation","deepMergeTranslations","filterLocales"],"mappings":"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"}
1
+ {"version":3,"file":"TranslationService.cjs.js","sources":["../../src/service/TranslationService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport fs from 'fs';\nimport path from 'path';\n\nimport { deepMergeTranslations, isValidJSONTranslation } from '../utils';\n\nexport interface TranslationFileStats {\n filesProcessed: number;\n filesNotFound: number;\n filesInvalid: number;\n}\n\nexport interface TranslationResult {\n translations: Record<string, any>;\n stats: {\n totalFilesProcessed: number;\n totalFilesNotFound: number;\n totalFilesInvalid: number;\n };\n}\n\n/**\n * Service for managing translation files and processing\n */\nexport class TranslationService {\n private cachedTranslations: Record<string, any> | null = null;\n private readonly overridesFiles: string[];\n\n constructor(\n private readonly config: Config,\n private readonly logger: LoggerService,\n ) {\n this.overridesFiles =\n this.config.getOptionalStringArray('i18n.overrides') ?? [];\n }\n\n /**\n * Finds the topmost folder with the given name by searching up the directory tree\n * Similar to the approach used in extensions plugin\n */\n private findTopmostFolder(\n folderName: string,\n startPath = process.cwd(),\n ): string | null {\n let currentPath = path.resolve(startPath);\n let topmostFoundPath: string | null = null;\n\n while (currentPath !== path.parse(currentPath).root) {\n const targetFolderPath = path.join(currentPath, folderName);\n if (\n fs.existsSync(targetFolderPath) &&\n fs.statSync(targetFolderPath).isDirectory()\n ) {\n topmostFoundPath = targetFolderPath;\n }\n currentPath = path.dirname(currentPath);\n }\n const targetFolderPath = path.join(currentPath, folderName);\n\n if (\n fs.existsSync(targetFolderPath) &&\n fs.statSync(targetFolderPath).isDirectory()\n ) {\n topmostFoundPath = targetFolderPath;\n }\n\n return topmostFoundPath;\n }\n\n /**\n * Resolves the internal translations directory path\n * Uses dynamic folder search to find the topmost 'translations' folder\n */\n private resolveInternalTranslationsDirectory(): string | null {\n // First try to find 'src/translations' in the topmost directory structure\n const srcTranslationsPath = this.findTopmostFolder('src');\n if (srcTranslationsPath) {\n const translationsPath = path.join(srcTranslationsPath, 'translations');\n if (\n fs.existsSync(translationsPath) &&\n fs.statSync(translationsPath).isDirectory()\n ) {\n this.logger.info(\n `Found internal translations directory: ${translationsPath}`,\n );\n return translationsPath;\n }\n }\n\n // Fallback: look for 'translations' folder directly\n const translationsPath = this.findTopmostFolder('translations');\n if (translationsPath) {\n this.logger.info(\n `Found internal translations directory: ${translationsPath}`,\n );\n return translationsPath;\n }\n\n this.logger.debug(\n 'No internal translations directory found in any expected location',\n );\n return null;\n }\n\n /**\n * Orders catalog files so higher-priority sources override earlier sources.\n */\n private orderCatalogFiles(files: string[]): string[] {\n const sourceOrder = [\n 'backstage-',\n 'community-plugins-',\n 'rhdh-plugins-',\n 'rhdh-',\n ];\n const priority = (file: string) => {\n const name = path.basename(file);\n const index = sourceOrder.findIndex(prefix => name.startsWith(prefix));\n // Unrecognized files are customer additions and should override shipped\n // catalogs when they define the same key.\n return index === -1 ? sourceOrder.length : index;\n };\n return files.sort(\n (a, b) => priority(a) - priority(b) || a.localeCompare(b),\n );\n }\n\n /**\n * Scans a directory for JSON translation files\n */\n private scanDirectoryForTranslationFiles(directoryPath: string): string[] {\n try {\n if (!fs.existsSync(directoryPath)) {\n this.logger.warn(\n `Internal translation directory not found: ${directoryPath}`,\n );\n return [];\n }\n\n const files = fs.readdirSync(directoryPath);\n const jsonFiles = files\n .filter(file => file.endsWith('.json'))\n .map(file => path.join(directoryPath, file));\n\n if (jsonFiles.length > 0) {\n this.logger.info(\n `Found ${jsonFiles.length} translation files in internal directory: ${directoryPath}`,\n );\n }\n\n return jsonFiles;\n } catch (error) {\n this.logger.warn(\n `Failed to scan internal directory ${directoryPath}: ${error}`,\n );\n return [];\n }\n }\n\n /**\n * Processes translation files and merges them into the target object\n */\n private processTranslationFiles(\n files: string[],\n mergedTranslations: Record<string, any>,\n source: string,\n ): TranslationFileStats {\n let filesProcessed = 0;\n let filesNotFound = 0;\n let filesInvalid = 0;\n\n for (const file of files) {\n try {\n if (!fs.existsSync(file)) {\n this.logger.warn(`Translation file not found: ${file} (${source})`);\n filesNotFound++;\n continue;\n }\n\n const raw = fs.readFileSync(file, 'utf8');\n const json = JSON.parse(raw);\n if (!isValidJSONTranslation(json)) {\n this.logger.warn(\n `Invalid JSON translation file format: ${file} (${source})`,\n );\n filesInvalid++;\n continue;\n }\n\n deepMergeTranslations(mergedTranslations, json);\n this.logger.debug(`Processed translation file: ${file} (${source})`);\n filesProcessed++;\n } catch (parseError) {\n this.logger.warn(\n `Failed to parse JSON from file: ${file} (${source}) - ${parseError}`,\n );\n filesInvalid++;\n }\n }\n\n return { filesProcessed, filesNotFound, filesInvalid };\n }\n\n /**\n * Gets all translations, processing both internal directory and config overrides\n */\n async getTranslations(): Promise<TranslationResult> {\n if (this.cachedTranslations) {\n return {\n translations: this.cachedTranslations,\n stats: {\n totalFilesProcessed: 0,\n totalFilesNotFound: 0,\n totalFilesInvalid: 0,\n },\n };\n }\n\n const mergedTranslations: Record<string, any> = {};\n let totalFilesProcessed = 0;\n let totalFilesNotFound = 0;\n let totalFilesInvalid = 0;\n\n // Step 1: Load JSON files from the repository or mounted translations\n // directory.\n let internalFiles: string[] = [];\n const internalDirectory = this.resolveInternalTranslationsDirectory();\n if (internalDirectory) {\n internalFiles = this.orderCatalogFiles(\n this.scanDirectoryForTranslationFiles(internalDirectory),\n );\n if (internalFiles.length > 0) {\n const internalStats = this.processTranslationFiles(\n internalFiles,\n mergedTranslations,\n 'internal directory',\n );\n totalFilesProcessed += internalStats.filesProcessed;\n totalFilesNotFound += internalStats.filesNotFound;\n totalFilesInvalid += internalStats.filesInvalid;\n }\n }\n\n // Step 2: Explicit config overrides take highest priority.\n if (this.overridesFiles.length > 0) {\n const overrideStats = this.processTranslationFiles(\n this.overridesFiles,\n mergedTranslations,\n 'config overrides',\n );\n totalFilesProcessed += overrideStats.filesProcessed;\n totalFilesNotFound += overrideStats.filesNotFound;\n totalFilesInvalid += overrideStats.filesInvalid;\n }\n\n // Determine if we have any files configured\n const totalFilesConfigured =\n internalFiles.length + this.overridesFiles.length;\n\n // If no files were configured, return empty response\n if (totalFilesConfigured === 0) {\n this.logger.info(\n 'No translation files found (neither auto-detected internal directory nor config overrides)',\n );\n this.cachedTranslations = {};\n return {\n translations: {},\n stats: {\n totalFilesProcessed: 0,\n totalFilesNotFound: 0,\n totalFilesInvalid: 0,\n },\n };\n }\n\n // If files were configured but none were found, throw error\n if (totalFilesNotFound === totalFilesConfigured) {\n this.logger.warn('All configured translation files were not found');\n throw new Error('All configured translation files were not found');\n }\n\n // If files were found but none contained valid translations, throw error\n if (totalFilesProcessed === 0) {\n this.logger.warn(\n 'No valid translation files found in the provided files',\n );\n throw new Error('No valid translation files found in the provided files');\n }\n\n this.cachedTranslations = mergedTranslations;\n\n // Log summary\n this.logger.info(\n `Translation processing complete: ${totalFilesProcessed} files processed, ${totalFilesNotFound} files not found, ${totalFilesInvalid} files invalid`,\n );\n\n return {\n translations: this.cachedTranslations,\n stats: {\n totalFilesProcessed,\n totalFilesNotFound,\n totalFilesInvalid,\n },\n };\n }\n\n /**\n * Clears the cached translations (useful for testing or when files change)\n */\n clearCache(): void {\n this.cachedTranslations = null;\n }\n}\n"],"names":["path","targetFolderPath","fs","translationsPath","isValidJSONTranslation","deepMergeTranslations"],"mappings":"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"}
package/dist/utils.cjs.js CHANGED
@@ -27,22 +27,7 @@ function deepMergeTranslations(target, source) {
27
27
  }
28
28
  return target;
29
29
  }
30
- function filterLocales(allTranslations, configuredLocales) {
31
- const filtered = {};
32
- for (const pluginId of Object.keys(allTranslations)) {
33
- for (const locale of configuredLocales) {
34
- if (allTranslations[pluginId][locale]) {
35
- filtered[pluginId] = {
36
- ...filtered[pluginId] ?? {},
37
- [locale]: allTranslations[pluginId][locale]
38
- };
39
- }
40
- }
41
- }
42
- return filtered;
43
- }
44
30
 
45
31
  exports.deepMergeTranslations = deepMergeTranslations;
46
- exports.filterLocales = filterLocales;
47
32
  exports.isValidJSONTranslation = isValidJSONTranslation;
48
33
  //# sourceMappingURL=utils.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.cjs.js","sources":["../src/utils.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nexport function isValidJSONTranslation(json: Record<string, any>): boolean {\n if (typeof json !== 'object' || json === null) return false;\n\n for (const [pluginRef, locales] of Object.entries(json)) {\n if (typeof pluginRef !== 'string') return false;\n if (typeof locales !== 'object' || locales === null) return false;\n\n for (const [locale, messages] of Object.entries(locales)) {\n if (typeof locale !== 'string') return false;\n if (typeof messages !== 'object' || messages === null) return false;\n\n for (const [k, v] of Object.entries(messages)) {\n if (typeof k !== 'string' || typeof v !== 'string') {\n return false;\n }\n }\n }\n }\n\n return true;\n}\n\nexport function deepMergeTranslations(\n target: Record<string, any>,\n source: Record<string, any>,\n): Record<string, any> {\n for (const key of Object.keys(source)) {\n if (\n source[key] &&\n typeof source[key] === 'object' &&\n !Array.isArray(source[key])\n ) {\n target[key] = deepMergeTranslations(target[key] || {}, source[key]);\n } else {\n target[key] = source[key];\n }\n }\n return target;\n}\n\nexport function filterLocales(\n allTranslations: Record<string, any>,\n configuredLocales: string[],\n): Record<string, any> {\n const filtered: Record<string, any> = {};\n for (const pluginId of Object.keys(allTranslations)) {\n for (const locale of configuredLocales) {\n if (allTranslations[pluginId][locale]) {\n filtered[pluginId] = {\n ...(filtered[pluginId] ?? {}),\n [locale]: allTranslations[pluginId][locale],\n };\n }\n }\n }\n\n return filtered;\n}\n"],"names":[],"mappings":";;AAeO,SAAS,uBAAuB,IAAA,EAAoC;AACzE,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,IAAA,KAAS,MAAM,OAAO,KAAA;AAEtD,EAAA,KAAA,MAAW,CAAC,SAAA,EAAW,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA,EAAG;AACvD,IAAA,IAAI,OAAO,SAAA,KAAc,QAAA,EAAU,OAAO,KAAA;AAC1C,IAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,OAAA,KAAY,MAAM,OAAO,KAAA;AAE5D,IAAA,KAAA,MAAW,CAAC,MAAA,EAAQ,QAAQ,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AACxD,MAAA,IAAI,OAAO,MAAA,KAAW,QAAA,EAAU,OAAO,KAAA;AACvC,MAAA,IAAI,OAAO,QAAA,KAAa,QAAA,IAAY,QAAA,KAAa,MAAM,OAAO,KAAA;AAE9D,MAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAC7C,QAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,OAAO,MAAM,QAAA,EAAU;AAClD,UAAA,OAAO,KAAA;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,qBAAA,CACd,QACA,MAAA,EACqB;AACrB,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA,EAAG;AACrC,IAAA,IACE,MAAA,CAAO,GAAG,CAAA,IACV,OAAO,OAAO,GAAG,CAAA,KAAM,QAAA,IACvB,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAA,CAAO,GAAG,CAAC,CAAA,EAC1B;AACA,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,qBAAA,CAAsB,MAAA,CAAO,GAAG,KAAK,EAAC,EAAG,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,IACpE,CAAA,MAAO;AACL,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,MAAA,CAAO,GAAG,CAAA;AAAA,IAC1B;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,aAAA,CACd,iBACA,iBAAA,EACqB;AACrB,EAAA,MAAM,WAAgC,EAAC;AACvC,EAAA,KAAA,MAAW,QAAA,IAAY,MAAA,CAAO,IAAA,CAAK,eAAe,CAAA,EAAG;AACnD,IAAA,KAAA,MAAW,UAAU,iBAAA,EAAmB;AACtC,MAAA,IAAI,eAAA,CAAgB,QAAQ,CAAA,CAAE,MAAM,CAAA,EAAG;AACrC,QAAA,QAAA,CAAS,QAAQ,CAAA,GAAI;AAAA,UACnB,GAAI,QAAA,CAAS,QAAQ,CAAA,IAAK,EAAC;AAAA,UAC3B,CAAC,MAAM,GAAG,eAAA,CAAgB,QAAQ,EAAE,MAAM;AAAA,SAC5C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,QAAA;AACT;;;;;;"}
1
+ {"version":3,"file":"utils.cjs.js","sources":["../src/utils.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nexport function isValidJSONTranslation(json: Record<string, any>): boolean {\n if (typeof json !== 'object' || json === null) return false;\n\n for (const [pluginRef, locales] of Object.entries(json)) {\n if (typeof pluginRef !== 'string') return false;\n if (typeof locales !== 'object' || locales === null) return false;\n\n for (const [locale, messages] of Object.entries(locales)) {\n if (typeof locale !== 'string') return false;\n if (typeof messages !== 'object' || messages === null) return false;\n\n for (const [k, v] of Object.entries(messages)) {\n if (typeof k !== 'string' || typeof v !== 'string') {\n return false;\n }\n }\n }\n }\n\n return true;\n}\n\nexport function deepMergeTranslations(\n target: Record<string, any>,\n source: Record<string, any>,\n): Record<string, any> {\n for (const key of Object.keys(source)) {\n if (\n source[key] &&\n typeof source[key] === 'object' &&\n !Array.isArray(source[key])\n ) {\n target[key] = deepMergeTranslations(target[key] || {}, source[key]);\n } else {\n target[key] = source[key];\n }\n }\n return target;\n}\n"],"names":[],"mappings":";;AAeO,SAAS,uBAAuB,IAAA,EAAoC;AACzE,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,IAAA,KAAS,MAAM,OAAO,KAAA;AAEtD,EAAA,KAAA,MAAW,CAAC,SAAA,EAAW,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA,EAAG;AACvD,IAAA,IAAI,OAAO,SAAA,KAAc,QAAA,EAAU,OAAO,KAAA;AAC1C,IAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,OAAA,KAAY,MAAM,OAAO,KAAA;AAE5D,IAAA,KAAA,MAAW,CAAC,MAAA,EAAQ,QAAQ,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AACxD,MAAA,IAAI,OAAO,MAAA,KAAW,QAAA,EAAU,OAAO,KAAA;AACvC,MAAA,IAAI,OAAO,QAAA,KAAa,QAAA,IAAY,QAAA,KAAa,MAAM,OAAO,KAAA;AAE9D,MAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAC7C,QAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,OAAO,MAAM,QAAA,EAAU;AAClD,UAAA,OAAO,KAAA;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,qBAAA,CACd,QACA,MAAA,EACqB;AACrB,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA,EAAG;AACrC,IAAA,IACE,MAAA,CAAO,GAAG,CAAA,IACV,OAAO,OAAO,GAAG,CAAA,KAAM,QAAA,IACvB,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAA,CAAO,GAAG,CAAC,CAAA,EAC1B;AACA,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,qBAAA,CAAsB,MAAA,CAAO,GAAG,KAAK,EAAC,EAAG,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,IACpE,CAAA,MAAO;AACL,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,MAAA,CAAO,GAAG,CAAA;AAAA,IAC1B;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/backstage-plugin-translations-backend",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,6 +9,7 @@
9
9
  "main": "dist/index.cjs.js",
10
10
  "types": "dist/index.d.ts"
11
11
  },
12
+ "configSchema": "config.schema.json",
12
13
  "repository": {
13
14
  "type": "git",
14
15
  "url": "https://github.com/redhat-developer/rhdh-plugins",
@@ -52,7 +53,8 @@
52
53
  "supertest": "^6.2.4"
53
54
  },
54
55
  "files": [
55
- "dist"
56
+ "dist",
57
+ "config.schema.json"
56
58
  ],
57
59
  "typesVersions": {
58
60
  "*": {