@sap-ux/project-access 2.2.2 → 2.3.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/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { FileName, DirName, FioriToolsSettings, MinCdsPluginUi5Version, MinCdsVersion, fioriToolsDirectory } from './constants.js';
2
2
  export { getFilePaths, findFilesByExtension } from './file/index.js';
3
3
  export { normalizePath } from './path/index.js';
4
- export { addPackageDevDependency, clearCdsModuleCache, createApplicationAccess, createProjectAccess, deleteCapApp, filterDataSourcesByType, findAllApps, findCapProjectRoot, findCapProjects, findFioriArtifacts, findProjectRoot, findRootsForPath, getAllUi5YamlFileNames, getAppRootFromWebappPath, getAppProgrammingLanguage, getAppType, getCapCustomPaths, getCapEnvironment, getCapModelAndServices, getCapServiceName, getCapProjectType, getCdsFiles, getCdsRoots, getCdsServices, getCapI18nFolderNames, getSpecification, getSpecificationModuleFromCache, getSpecificationPath, getI18nPropertiesPaths, getI18nBundles, getMinUI5VersionFromManifest, getMinUI5VersionAsArray, getMinimumUI5Version, getMtaPath, getMockServerConfig, getMockDataPath, getNodeModulesPath, getPathMappings, getProject, getProjectType, getWebappPath, hasUI5CliV3, isCapProject, isCapJavaProject, isCapNodeJsProject, loadModuleFromProject, readCapServiceMetadataEdmx, readUi5Yaml, refreshSpecificationDistTags, toReferenceUri, updatePackageScript, getWorkspaceInfo, hasMinCdsVersion, checkCdsUi5PluginEnabled, readFlexChanges, processServices, getMainService, getGlobalCdsHomePath, isUI5IdUnique, getUsedEntitiesFromManifest, resolveApplicationTitle } from './project/index.js';
4
+ export { addPackageDevDependency, clearCdsModuleCache, createApplicationAccess, createProjectAccess, deleteCapApp, filterDataSourcesByType, findAllApps, findCapProjectRoot, findCapProjects, findFioriArtifacts, findProjectRoot, findRootsForPath, getAllUi5YamlFileNames, getAppRootFromWebappPath, getAppProgrammingLanguage, getAppType, getCapCustomPaths, getCapEnvironment, getCapModelAndServices, getCapServiceName, getCapProjectType, getCdsFiles, getCdsRoots, getCdsServices, getCapI18nFolderNames, getSpecification, getSpecificationModuleFromCache, getSpecificationPath, getI18nPropertiesPaths, getI18nBundles, createI18nEntriesAtPath, getMinUI5VersionFromManifest, getMinUI5VersionAsArray, getMinimumUI5Version, getMtaPath, getMockServerConfig, getMockDataPath, getNodeModulesPath, getPathMappings, getProject, getProjectType, getWebappPath, hasUI5CliV3, isCapProject, isCapJavaProject, isCapNodeJsProject, loadModuleFromProject, readCapServiceMetadataEdmx, readUi5Yaml, refreshSpecificationDistTags, toReferenceUri, updatePackageScript, getWorkspaceInfo, hasMinCdsVersion, checkCdsUi5PluginEnabled, readFlexChanges, processServices, getMainService, getGlobalCdsHomePath, isUI5IdUnique, getUsedEntitiesFromManifest, resolveApplicationTitle } from './project/index.js';
5
5
  export type { UsedEntity } from './project/index.js';
6
6
  export { execNpmCommand } from './command/npm-command.js';
7
7
  export * from './types/index.js';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { FileName, DirName, FioriToolsSettings, MinCdsPluginUi5Version, MinCdsVersion, fioriToolsDirectory } from './constants.js';
2
2
  export { getFilePaths, findFilesByExtension } from './file/index.js';
3
3
  export { normalizePath } from './path/index.js';
4
- export { addPackageDevDependency, clearCdsModuleCache, createApplicationAccess, createProjectAccess, deleteCapApp, filterDataSourcesByType, findAllApps, findCapProjectRoot, findCapProjects, findFioriArtifacts, findProjectRoot, findRootsForPath, getAllUi5YamlFileNames, getAppRootFromWebappPath, getAppProgrammingLanguage, getAppType, getCapCustomPaths, getCapEnvironment, getCapModelAndServices, getCapServiceName, getCapProjectType, getCdsFiles, getCdsRoots, getCdsServices, getCapI18nFolderNames, getSpecification, getSpecificationModuleFromCache, getSpecificationPath, getI18nPropertiesPaths, getI18nBundles, getMinUI5VersionFromManifest, getMinUI5VersionAsArray, getMinimumUI5Version, getMtaPath, getMockServerConfig, getMockDataPath, getNodeModulesPath, getPathMappings, getProject, getProjectType, getWebappPath, hasUI5CliV3, isCapProject, isCapJavaProject, isCapNodeJsProject, loadModuleFromProject, readCapServiceMetadataEdmx, readUi5Yaml, refreshSpecificationDistTags, toReferenceUri, updatePackageScript, getWorkspaceInfo, hasMinCdsVersion, checkCdsUi5PluginEnabled, readFlexChanges, processServices, getMainService, getGlobalCdsHomePath, isUI5IdUnique, getUsedEntitiesFromManifest, resolveApplicationTitle } from './project/index.js';
4
+ export { addPackageDevDependency, clearCdsModuleCache, createApplicationAccess, createProjectAccess, deleteCapApp, filterDataSourcesByType, findAllApps, findCapProjectRoot, findCapProjects, findFioriArtifacts, findProjectRoot, findRootsForPath, getAllUi5YamlFileNames, getAppRootFromWebappPath, getAppProgrammingLanguage, getAppType, getCapCustomPaths, getCapEnvironment, getCapModelAndServices, getCapServiceName, getCapProjectType, getCdsFiles, getCdsRoots, getCdsServices, getCapI18nFolderNames, getSpecification, getSpecificationModuleFromCache, getSpecificationPath, getI18nPropertiesPaths, getI18nBundles, createI18nEntriesAtPath, getMinUI5VersionFromManifest, getMinUI5VersionAsArray, getMinimumUI5Version, getMtaPath, getMockServerConfig, getMockDataPath, getNodeModulesPath, getPathMappings, getProject, getProjectType, getWebappPath, hasUI5CliV3, isCapProject, isCapJavaProject, isCapNodeJsProject, loadModuleFromProject, readCapServiceMetadataEdmx, readUi5Yaml, refreshSpecificationDistTags, toReferenceUri, updatePackageScript, getWorkspaceInfo, hasMinCdsVersion, checkCdsUi5PluginEnabled, readFlexChanges, processServices, getMainService, getGlobalCdsHomePath, isUI5IdUnique, getUsedEntitiesFromManifest, resolveApplicationTitle } from './project/index.js';
5
5
  export { execNpmCommand } from './command/npm-command.js';
6
6
  export * from './types/index.js';
7
7
  export { checkDependencies, getReuseLibs } from './library/index.js';
@@ -1,5 +1,5 @@
1
1
  import { join, relative } from 'node:path';
2
- import { getI18nBundles, getI18nPropertiesPaths, createCapI18nEntries, createManifestI18nEntries, createUI5I18nEntries, createAnnotationI18nEntries } from './i18n/index.js';
2
+ import { getI18nBundles, getI18nPropertiesPaths, createCapI18nEntries, createManifestI18nEntries, createUI5I18nEntries, createAnnotationI18nEntries, createI18nEntriesAtPath } from './i18n/index.js';
3
3
  import { getProject } from './info.js';
4
4
  import { findAllApps } from './search.js';
5
5
  import { readFile, readJSON, updateManifestJSON, updatePackageJSON } from '../file/index.js';
@@ -102,6 +102,17 @@ class ApplicationAccessImp {
102
102
  createCapI18nEntries(filePath, newI18nEntries) {
103
103
  return createCapI18nEntries(this.project.root, filePath, newI18nEntries, this.options?.fs);
104
104
  }
105
+ /**
106
+ * Maintains new translation entries at a specific i18n properties file path.
107
+ * Use this when the target file is already known (e.g. chosen by the user from a picker).
108
+ *
109
+ * @param filePath absolute path to the target `.properties` file
110
+ * @param newEntries translation entries to write
111
+ * @returns boolean or exception
112
+ */
113
+ createI18nEntriesAtPath(filePath, newEntries) {
114
+ return createI18nEntriesAtPath(this.project.root, filePath, newEntries, this.options?.fs);
115
+ }
105
116
  /**
106
117
  * Return the application id of this app, which is the relative path from the project root
107
118
  * to the app root.
@@ -16,10 +16,17 @@ export async function getI18nPropertiesPaths(manifestPath, manifest, memFs) {
16
16
  'sap.app': join(manifestFolder, relativeI18nPropertiesPaths['sap.app']),
17
17
  models: {}
18
18
  };
19
+ if (relativeI18nPropertiesPaths['sap.app.fallbackLocale']) {
20
+ i18nPropertiesPaths['sap.app.fallbackLocale'] = join(manifestFolder, relativeI18nPropertiesPaths['sap.app.fallbackLocale']);
21
+ }
19
22
  for (const modelKey in relativeI18nPropertiesPaths.models) {
23
+ const relativeModelPaths = relativeI18nPropertiesPaths.models[modelKey];
20
24
  i18nPropertiesPaths.models[modelKey] = {
21
- path: join(manifestFolder, relativeI18nPropertiesPaths.models[modelKey].path)
25
+ path: join(manifestFolder, relativeModelPaths.path)
22
26
  };
27
+ if (relativeModelPaths.fallbackLocalePath) {
28
+ i18nPropertiesPaths.models[modelKey].fallbackLocalePath = join(manifestFolder, relativeModelPaths.fallbackLocalePath);
29
+ }
23
30
  }
24
31
  return i18nPropertiesPaths;
25
32
  }
@@ -31,10 +38,45 @@ export async function getI18nPropertiesPaths(manifestPath, manifest, memFs) {
31
38
  * @returns - paths to i18n.properties files from sap.app and models
32
39
  */
33
40
  export function getRelativeI18nPropertiesPaths(manifest) {
34
- return {
35
- 'sap.app': getI18nAppPath(manifest),
41
+ const appPath = getI18nAppPath(manifest);
42
+ const appFallbackLocalePath = getI18nAppFallbackLocalePath(manifest, appPath);
43
+ const result = {
44
+ 'sap.app': appPath,
36
45
  models: getI18nModelPaths(manifest)
37
46
  };
47
+ if (appFallbackLocalePath) {
48
+ result['sap.app.fallbackLocale'] = appFallbackLocalePath;
49
+ }
50
+ return result;
51
+ }
52
+ /**
53
+ * Computes the fallback locale variant of a base i18n file path, e.g. "i18n/i18n.properties" + "en" yields "i18n/i18n_en.properties".
54
+ *
55
+ * @param basePath - base i18n file path
56
+ * @param locale - fallback locale string (e.g. "en", "de")
57
+ * @returns - path to the fallback locale file
58
+ */
59
+ function computeFallbackLocalePath(basePath, locale) {
60
+ const ext = '.properties';
61
+ const stem = basePath.endsWith(ext) ? basePath.slice(0, -ext.length) : basePath;
62
+ return `${stem}_${locale}${ext}`;
63
+ }
64
+ /**
65
+ * Get the fallback locale i18n path from sap.app.i18n part of the manifest.
66
+ *
67
+ * @param manifest - parsed content of manifest.json
68
+ * @param appPath - resolved base i18n path for sap.app
69
+ * @returns - fallback locale path, or undefined if not configured
70
+ */
71
+ function getI18nAppFallbackLocalePath(manifest, appPath) {
72
+ const i18n = manifest?.['sap.app']?.i18n;
73
+ if (i18n !== null && typeof i18n === 'object') {
74
+ const fallbackLocale = i18n.fallbackLocale;
75
+ if (typeof fallbackLocale === 'string' && fallbackLocale && /^[A-Za-z0-9_-]+$/.test(fallbackLocale)) {
76
+ return computeFallbackLocalePath(appPath, fallbackLocale);
77
+ }
78
+ }
79
+ return undefined;
38
80
  }
39
81
  /**
40
82
  * Get the i18n path from sap.app.i18n part of the manifest.
@@ -52,61 +94,91 @@ function getI18nAppPath(manifest) {
52
94
  if (typeof manifest?.['sap.app']?.i18n === 'string') {
53
95
  return join(manifest['sap.app'].i18n);
54
96
  }
55
- if (typeof manifest?.['sap.app']?.i18n === 'object') {
97
+ const i18nObj = manifest?.['sap.app']?.i18n;
98
+ if (i18nObj !== null && typeof i18nObj === 'object') {
56
99
  // bundleName wins over `bundleUrl`
57
- if ('bundleName' in manifest['sap.app'].i18n) {
58
- // module name is in dot notation
59
- const withoutAppId = manifest['sap.app'].i18n.bundleName.replace(manifest['sap.app'].id, '');
60
- const i18nPath = `${join(...withoutAppId.split('.'))}.properties`;
100
+ if ('bundleName' in i18nObj) {
101
+ // module name is in dot notation; strip appId as a prefix
102
+ const bundleName = i18nObj.bundleName;
103
+ const appId = manifest['sap.app'].id ?? '';
104
+ const suffix = appId && bundleName.startsWith(appId) ? bundleName.slice(appId.length) : bundleName;
105
+ if (!suffix) {
106
+ return defaultPath;
107
+ }
108
+ const i18nPath = `${join(...suffix.split('.'))}.properties`;
61
109
  return join(i18nPath);
62
110
  }
63
- if ('bundleUrl' in manifest['sap.app'].i18n) {
64
- return join(manifest['sap.app'].i18n.bundleUrl);
111
+ if ('bundleUrl' in i18nObj) {
112
+ return join(i18nObj.bundleUrl);
65
113
  }
66
114
  }
67
115
  // default
68
116
  return defaultPath;
69
117
  }
70
118
  /**
71
- * Get the i18n path from UI5 resource models declared in sap.ui5.models part of the manifest.
72
- * By default the model used for internationalization in the UI is 'i18n'. For
73
- * internationalization of annotations the model is '@18n'.
119
+ * Resolves the i18n file path from a resource model's settings object.
74
120
  *
75
- * for For `sap.ui5` namespace
76
- * 1. from `sap.ui5.models.{resource model key}.bundleName` as `bundleName` wins over `bundleUrl`
77
- * 2. from `sap.ui5.models.{resource model key}.bundleUrl`
78
- * 3. from `sap.ui5.models.{resource model key}.uri`
121
+ * @param appId - application id from sap.app namespace
122
+ * @param settings - resource model settings object
123
+ * @param settings.bundleName - bundle name in dot notation (e.g. "sample.app.i18n")
124
+ * @param settings.bundleUrl - bundle URL path (e.g. "i18n/i18n.properties")
125
+ * @returns - resolved relative path, or undefined if it cannot be determined
126
+ */
127
+ function extractBundlePath(appId, settings) {
128
+ if (settings.bundleName) {
129
+ // Strip appId as a prefix when present; fall back to full bundleName otherwise.
130
+ // Prefix-strip is safer than String.replace which would remove the first occurrence anywhere in the string.
131
+ const suffix = appId && settings.bundleName.startsWith(appId)
132
+ ? settings.bundleName.slice(appId.length)
133
+ : settings.bundleName;
134
+ if (!suffix) {
135
+ return undefined;
136
+ }
137
+ return `${join(...suffix.split('.'))}.properties`;
138
+ }
139
+ if (settings.bundleUrl) {
140
+ return join(settings.bundleUrl);
141
+ }
142
+ return undefined;
143
+ }
144
+ /**
145
+ * Extracts and validates the `fallbackLocale` from a UI5 resource model settings object.
146
+ * Only alphanumeric, dash, and underscore characters are accepted (e.g. "en", "en-US").
79
147
  *
80
- * @param manifest - parsed content of manifest.json
81
- * @returns - paths to i18n.properties file from models
148
+ * @param settings - sap.ui5 resource model settings object
149
+ * @returns validated fallback locale string or undefined if invalid/absent
82
150
  */
151
+ function getValidFallbackLocale(settings) {
152
+ const fallbackLocale = settings?.fallbackLocale;
153
+ return typeof fallbackLocale === 'string' && fallbackLocale && /^[A-Za-z0-9_-]+$/.test(fallbackLocale)
154
+ ? fallbackLocale
155
+ : undefined;
156
+ }
83
157
  function getI18nModelPaths(manifest) {
84
158
  const result = {};
85
159
  const models = manifest?.['sap.ui5']?.models ?? {};
86
160
  const resourceModelKeys = Object.keys(models).filter((key) => models[key].type === 'sap.ui.model.resource.ResourceModel');
87
161
  for (const modelKey of resourceModelKeys) {
88
162
  const i18nModel = models[modelKey];
89
- // settings wins over `uri`
90
163
  if (i18nModel.settings) {
91
- // bundleName wins over `bundleUrl`
92
- if (i18nModel.settings.bundleName) {
93
- // module name is in dot notation
94
- const appId = manifest['sap.app']?.id ?? '';
95
- if (!appId) {
96
- continue;
97
- }
98
- const withoutAppId = i18nModel.settings.bundleName.replace(appId, '');
99
- const i18nPath = `${join(...withoutAppId.split('.'))}.properties`;
100
- result[modelKey] = { path: join(i18nPath) };
101
- continue;
102
- }
103
- if (i18nModel.settings.bundleUrl) {
104
- result[modelKey] = { path: join(i18nModel.settings.bundleUrl) };
164
+ const appId = manifest['sap.app']?.id ?? '';
165
+ const path = extractBundlePath(appId, i18nModel.settings);
166
+ if (path) {
167
+ const fallbackLocale = getValidFallbackLocale(i18nModel.settings);
168
+ result[modelKey] = {
169
+ path,
170
+ ...(fallbackLocale && { fallbackLocalePath: computeFallbackLocalePath(path, fallbackLocale) })
171
+ };
105
172
  continue;
106
173
  }
107
174
  }
108
175
  if (i18nModel.uri) {
109
- result[modelKey] = { path: join(i18nModel.uri) };
176
+ const path = join(i18nModel.uri);
177
+ const fallbackLocale = getValidFallbackLocale(i18nModel.settings);
178
+ result[modelKey] = {
179
+ path,
180
+ ...(fallbackLocale && { fallbackLocalePath: computeFallbackLocalePath(path, fallbackLocale) })
181
+ };
110
182
  }
111
183
  }
112
184
  return result;
@@ -1,4 +1,4 @@
1
1
  export { getI18nPropertiesPaths } from './i18n.js';
2
2
  export { getCapI18nFolderNames, getI18nBundles } from './read.js';
3
- export { createManifestI18nEntries, createUI5I18nEntries, createAnnotationI18nEntries, createCapI18nEntries } from './write.js';
3
+ export { createManifestI18nEntries, createUI5I18nEntries, createAnnotationI18nEntries, createCapI18nEntries, createI18nEntriesAtPath } from './write.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,4 @@
1
1
  export { getI18nPropertiesPaths } from './i18n.js';
2
2
  export { getCapI18nFolderNames, getI18nBundles } from './read.js';
3
- export { createManifestI18nEntries, createUI5I18nEntries, createAnnotationI18nEntries, createCapI18nEntries } from './write.js';
3
+ export { createManifestI18nEntries, createUI5I18nEntries, createAnnotationI18nEntries, createCapI18nEntries, createI18nEntriesAtPath } from './write.js';
4
4
  //# sourceMappingURL=index.js.map
@@ -8,11 +8,48 @@ import { getCapEnvironment, getCdsFiles } from '../index.js';
8
8
  * @param error error to add
9
9
  */
10
10
  function addToErrors(result, key, error) {
11
- if (!result.errors) {
12
- result.errors = {};
13
- }
11
+ result.errors ??= {};
14
12
  result.errors[key] = error;
15
13
  }
14
+ /**
15
+ * Merges a fallback locale bundle into a primary bundle.
16
+ * Skipped when the primary failed with a non-ENOENT error (file exists but unreadable).
17
+ * Clears the primary ENOENT error when the fallback is read successfully.
18
+ * Stores non-ENOENT fallback errors in result.errors instead of throwing.
19
+ *
20
+ * @param result - accumulator for bundles and errors
21
+ * @param primaryKey - error key used to look up the primary error (e.g. 'sap.app')
22
+ * @param current - primary bundle (may be empty if primary failed)
23
+ * @param fallbackPath - path to the fallback locale .properties file
24
+ * @param fs - optional mem-fs-editor instance
25
+ * @returns merged bundle (fallback keys supplemented by primary entries)
26
+ */
27
+ async function mergeWithFallback(result, primaryKey, current, fallbackPath, fs) {
28
+ const primaryError = result.errors?.[primaryKey];
29
+ if (primaryError && primaryError.code !== 'ENOENT') {
30
+ return current;
31
+ }
32
+ try {
33
+ const fallbackBundle = await getPropertiesI18nBundle(fallbackPath, fs);
34
+ if (primaryError?.code === 'ENOENT') {
35
+ const errors = result.errors;
36
+ if (errors) {
37
+ delete errors[primaryKey];
38
+ if (Object.keys(errors).length === 0) {
39
+ result.errors = undefined;
40
+ }
41
+ }
42
+ }
43
+ // Primary entries take precedence on key collision
44
+ return { ...fallbackBundle, ...current };
45
+ }
46
+ catch (error) {
47
+ if (error.code !== 'ENOENT') {
48
+ addToErrors(result, `${primaryKey}.fallbackLocale`, error);
49
+ }
50
+ return current;
51
+ }
52
+ }
16
53
  /**
17
54
  * For a given app in project, retrieves i18n bundles for 'sap.app' namespace,`models` of `sap.ui5` namespace and service for cap services.
18
55
  *
@@ -36,7 +73,10 @@ export async function getI18nBundles(root, i18nPropertiesPaths, projectType, fs)
36
73
  catch (error) {
37
74
  addToErrors(result, 'sap.app', error);
38
75
  }
39
- for (const key of Object.keys(i18nPropertiesPaths.models)) {
76
+ if (i18nPropertiesPaths['sap.app.fallbackLocale']) {
77
+ result['sap.app'] = await mergeWithFallback(result, 'sap.app', result['sap.app'], i18nPropertiesPaths['sap.app.fallbackLocale'], fs);
78
+ }
79
+ await Promise.all(Object.keys(i18nPropertiesPaths.models).map(async (key) => {
40
80
  try {
41
81
  result.models[key] = await getPropertiesI18nBundle(i18nPropertiesPaths.models[key].path, fs);
42
82
  }
@@ -45,7 +85,11 @@ export async function getI18nBundles(root, i18nPropertiesPaths, projectType, fs)
45
85
  result.models[key] = {};
46
86
  addToErrors(result, `models.${key}`, error);
47
87
  }
48
- }
88
+ const fallbackLocalePath = i18nPropertiesPaths.models[key].fallbackLocalePath;
89
+ if (fallbackLocalePath) {
90
+ result.models[key] = await mergeWithFallback(result, `models.${key}`, result.models[key], fallbackLocalePath, fs);
91
+ }
92
+ }));
49
93
  if (projectType === 'CAPJava' || projectType === 'CAPNodejs') {
50
94
  try {
51
95
  const env = await getCapEnvironment(root);
@@ -64,11 +64,22 @@ export declare function createUI5I18nEntries(root: string, manifestPath: string,
64
64
  * ```
65
65
  */
66
66
  export declare function createAnnotationI18nEntries(root: string, manifestPath: string, i18nPropertiesPaths: I18nPropertiesPaths, newEntries: NewI18nEntry[], fs?: Editor): Promise<boolean>;
67
+ /**
68
+ * Maintains new translation entries at a specific i18n properties file path.
69
+ * Use this when the target file is already known (e.g. chosen by the user from a picker).
70
+ *
71
+ * @param root project root
72
+ * @param filePath absolute path to the target `.properties` file
73
+ * @param newEntries translation entries to write
74
+ * @param fs optional `mem-fs-editor` instance
75
+ * @returns boolean or exception
76
+ */
77
+ export declare function createI18nEntriesAtPath(root: string, filePath: string, newEntries: NewI18nEntry[], fs?: Editor): Promise<boolean>;
67
78
  /**
68
79
  * Maintains new translation entries in an existing i18n file or in a new i18n properties file if it does not exist.
69
80
  *
70
81
  * @param root project root
71
- * @param i18nPropertiesPaths paths to `.properties` file`
82
+ * @param i18nPropertiesPaths paths to `.properties` file
72
83
  * @param newEntries translation entries to write in the `.properties` file
73
84
  * @param fs optional `mem-fs-editor` instance. If provided, `mem-fs-editor` api is used instead of `fs` of node
74
85
  * @returns boolean or exception
@@ -118,11 +118,28 @@ export async function createUI5I18nEntries(root, manifestPath, i18nPropertiesPat
118
118
  export async function createAnnotationI18nEntries(root, manifestPath, i18nPropertiesPaths, newEntries, fs) {
119
119
  return createUI5I18nEntriesBase(root, manifestPath, i18nPropertiesPaths, newEntries, '@i18n', fs);
120
120
  }
121
+ /**
122
+ * Maintains new translation entries at a specific i18n properties file path.
123
+ * Use this when the target file is already known (e.g. chosen by the user from a picker).
124
+ *
125
+ * @param root project root
126
+ * @param filePath absolute path to the target `.properties` file
127
+ * @param newEntries translation entries to write
128
+ * @param fs optional `mem-fs-editor` instance
129
+ * @returns boolean or exception
130
+ */
131
+ export async function createI18nEntriesAtPath(root, filePath, newEntries, fs) {
132
+ const dirPath = dirname(filePath);
133
+ if (!fs) {
134
+ await mkdir(dirPath, { recursive: true });
135
+ }
136
+ return createPropertiesI18nEntries(filePath, newEntries, root, fs);
137
+ }
121
138
  /**
122
139
  * Maintains new translation entries in an existing i18n file or in a new i18n properties file if it does not exist.
123
140
  *
124
141
  * @param root project root
125
- * @param i18nPropertiesPaths paths to `.properties` file`
142
+ * @param i18nPropertiesPaths paths to `.properties` file
126
143
  * @param newEntries translation entries to write in the `.properties` file
127
144
  * @param fs optional `mem-fs-editor` instance. If provided, `mem-fs-editor` api is used instead of `fs` of node
128
145
  * @returns boolean or exception
@@ -2,7 +2,7 @@ export { clearCdsModuleCache, deleteCapApp, getCapCustomPaths, getCapEnvironment
2
2
  export { filterDataSourcesByType, getMainService, getUsedEntitiesFromManifest } from './service.js';
3
3
  export type { UsedEntity } from './service.js';
4
4
  export { addPackageDevDependency, getNodeModulesPath, hasDependency } from './dependencies.js';
5
- export { getCapI18nFolderNames, getI18nPropertiesPaths, getI18nBundles } from './i18n/index.js';
5
+ export { getCapI18nFolderNames, getI18nPropertiesPaths, getI18nBundles, createI18nEntriesAtPath } from './i18n/index.js';
6
6
  export { getAppProgrammingLanguage, getAppType, getMinUI5VersionFromManifest, getMinUI5VersionAsArray, getMinimumUI5Version, getProject, getProjectType } from './info.js';
7
7
  export { loadModuleFromProject } from './module-loader.js';
8
8
  export { findAllApps, findCapProjects, findFioriArtifacts, findProjectRoot, getAppRootFromWebappPath, findCapProjectRoot, findRootsForPath } from './search.js';
@@ -1,7 +1,7 @@
1
1
  export { clearCdsModuleCache, deleteCapApp, getCapCustomPaths, getCapEnvironment, getCapModelAndServices, getCapProjectType, getCapServiceName, getCdsFiles, getCdsRoots, getCdsServices, isCapProject, isCapJavaProject, isCapNodeJsProject, readCapServiceMetadataEdmx, toReferenceUri, getWorkspaceInfo, hasMinCdsVersion, checkCdsUi5PluginEnabled, processServices, getGlobalCdsHomePath } from './cap.js';
2
2
  export { filterDataSourcesByType, getMainService, getUsedEntitiesFromManifest } from './service.js';
3
3
  export { addPackageDevDependency, getNodeModulesPath, hasDependency } from './dependencies.js';
4
- export { getCapI18nFolderNames, getI18nPropertiesPaths, getI18nBundles } from './i18n/index.js';
4
+ export { getCapI18nFolderNames, getI18nPropertiesPaths, getI18nBundles, createI18nEntriesAtPath } from './i18n/index.js';
5
5
  export { getAppProgrammingLanguage, getAppType, getMinUI5VersionFromManifest, getMinUI5VersionAsArray, getMinimumUI5Version, getProject, getProjectType } from './info.js';
6
6
  export { loadModuleFromProject } from './module-loader.js';
7
7
  export { findAllApps, findCapProjects, findFioriArtifacts, findProjectRoot, getAppRootFromWebappPath, findCapProjectRoot, findRootsForPath } from './search.js';
@@ -74,6 +74,15 @@ export interface ApplicationAccess extends BaseAccess {
74
74
  * @returns boolean or exception
75
75
  */
76
76
  createCapI18nEntries(filePath: string, newI18nEntries: NewI18nEntry[]): Promise<boolean>;
77
+ /**
78
+ * Maintains new translation entries at a specific i18n properties file path.
79
+ * Use this when the target file is already known (e.g. chosen by the user from a picker).
80
+ *
81
+ * @param filePath absolute path to the target `.properties` file
82
+ * @param newEntries translation entries to write
83
+ * @returns boolean or exception
84
+ */
85
+ createI18nEntriesAtPath(filePath: string, newEntries: NewI18nEntry[]): Promise<boolean>;
77
86
  /**
78
87
  * Return the application id of this app, which is the relative path from the project root
79
88
  * to the app root.
@@ -59,12 +59,22 @@ export interface I18nPropertiesPaths {
59
59
  * Absolute path to the i18n.properties file from sap.app namespace.
60
60
  */
61
61
  'sap.app': string;
62
+ /**
63
+ * Absolute path to the fallback locale i18n file from sap.app namespace, e.g. i18n_en.properties.
64
+ * Only set when `fallbackLocale` is configured in `sap.app.i18n`.
65
+ */
66
+ 'sap.app.fallbackLocale'?: string;
62
67
  models: {
63
68
  [modelKey: string]: {
64
69
  /**
65
70
  * Absolute path to the i18n.properties files.
66
71
  */
67
72
  path: string;
73
+ /**
74
+ * Absolute path to the fallback locale i18n file, e.g. i18n_en.properties.
75
+ * Only set when `fallbackLocale` is configured in the model settings.
76
+ */
77
+ fallbackLocalePath?: string;
68
78
  };
69
79
  };
70
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap-ux/project-access",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "Library to access SAP Fiori tools projects",
5
5
  "type": "module",
6
6
  "repository": {