From f8dc7c374e39d59f821c0421648281ee70bd5f9a Mon Sep 17 00:00:00 2001
From: bsrdjan <srdjan.boskovic@sap.com>
Date: Sun, 31 Jan 2021 20:13:44 +0100
Subject: [PATCH] i18n

---
 abap-api-tools/README.md         |  50 +++++++++++-
 abap-api-tools/package-lock.json |   4 +-
 abap-api-tools/package.json      |   2 +-
 abap-api-tools/src/ts/abap.ts    |  12 ++-
 abap-api-tools/src/ts/backend.ts | 129 +++++++++++++++++++++++++------
 5 files changed, 167 insertions(+), 30 deletions(-)

diff --git a/abap-api-tools/README.md b/abap-api-tools/README.md
index 694c0608..14bd8eb0 100644
--- a/abap-api-tools/README.md
+++ b/abap-api-tools/README.md
@@ -20,6 +20,7 @@ Command line tool for pattern based applications with ABAP/HANA systems.
   - [ABAP API annotations for ui elements](#abap-api-annotations-for-ui-elements)
   - [ui elements](#ui-elements)
 - [Custom ui configurations](#custom-ui-configurations)
+- [i18n](#i18n)
 - [Known Issues](#known-issues)
 - [Getting Support](#getting-support)
 - [Contributing](#contributing)
@@ -269,7 +270,7 @@ datepicker: >-
 You can edit both config files and use them with `make` command:
 
 ```shell
-abap make my-ui5 -c planned_order_api
+abap make my-ui5 -c my-api
 ```
 
 Elements with tilde prefix `~` are placeholders for texts, data binding and value input helps, described in standard ui configuration `yaml` file.
@@ -280,6 +281,53 @@ Custom configuration with the same name as standard one, if present in local fol
 abap rm my-ui5
 ```
 
+## i18n
+
+Texts for i18n translations are saved in `texts.yaml`, for the language used in `get` command:
+
+```shell
+abap get MME -c my-api # default lang = en
+```
+
+`texts.yaml`
+
+```yaml
+en: City postal code
+short:
+  en:
+    FIELDTEXT: City postal code
+    REPTEXT: Postl Code
+    SCRTEXT_L: Postal Code
+    SCRTEXT_M: Postal Code
+    SCRTEXT_S: Postl Code
+```
+
+Texts in additional languages are added using `-t|--text-only` boolean option:
+
+```shell
+abap get MME -c my-api --text-only --lang de
+```
+
+`texts.yaml`
+
+```yaml
+de: Postleitzahl des Orts
+en: City postal code
+short:
+  de:
+    FIELDTEXT: Postleitzahl des Orts
+    REPTEXT: PLZ
+    SCRTEXT_L: Postleitzahl
+    SCRTEXT_M: Postleitzahl
+    SCRTEXT_S: PLZ
+  en:
+    FIELDTEXT: City postal code
+    REPTEXT: Postl Code
+    SCRTEXT_L: Postal Code
+    SCRTEXT_M: Postal Code
+    SCRTEXT_S: Postl Code
+```
+
 ## Known Issues
 
 Click [here](https://github.com/SAP/fundamental-toolset/issues) to view the current issues.
diff --git a/abap-api-tools/package-lock.json b/abap-api-tools/package-lock.json
index 50562c7f..c534dd74 100644
--- a/abap-api-tools/package-lock.json
+++ b/abap-api-tools/package-lock.json
@@ -1,11 +1,11 @@
 {
   "name": "abap-api-tools",
-  "version": "1.1.0",
+  "version": "1.2.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
-      "version": "1.1.0",
+      "version": "1.2.0",
       "cpu": [
         "!arm"
       ],
diff --git a/abap-api-tools/package.json b/abap-api-tools/package.json
index 696c7837..b8b8b3fc 100644
--- a/abap-api-tools/package.json
+++ b/abap-api-tools/package.json
@@ -1,7 +1,7 @@
 {
   "name": "abap-api-tools",
   "description": "ABAP api tools",
-  "version": "1.1.0",
+  "version": "1.2.0",
   "homepage": "https://github.com/sap/fundamental-tools",
   "author": "SAP",
   "license": "Apache-2.0",
diff --git a/abap-api-tools/src/ts/abap.ts b/abap-api-tools/src/ts/abap.ts
index 94cd6c6a..d65b3a58 100644
--- a/abap-api-tools/src/ts/abap.ts
+++ b/abap-api-tools/src/ts/abap.ts
@@ -43,6 +43,7 @@ export interface Arguments {
   output: string;
   lang: string;
   save: boolean;
+  textOnly: boolean;
 }
 
 class CliHandler {
@@ -89,7 +90,10 @@ class CliHandler {
           abap = await backend.parse();
         }
 
-        if ([Command.call, Command.get, Command.make].includes(this.argv.cmd)) {
+        if (
+          (this.argv.cmd === Command.get && !this.argv.textOnly) ||
+          [Command.call, Command.make].includes(this.argv.cmd)
+        ) {
           const frontend = new Frontend(api_name, abap, this.argv);
           log.debug(`frontend run ${api_name}`);
           frontend.parse();
@@ -197,6 +201,12 @@ export const argv = yargs(process.argv.slice(2))
           alias: "catalog",
           describe: "Read RFM names from file",
         })
+        .option("t", {
+          alias: "text-only",
+          type: "boolean",
+          default: false,
+          describe: "Get only texts in a given language",
+        })
         .option("o", {
           alias: "output",
           describe: "Output folder",
diff --git a/abap-api-tools/src/ts/backend.ts b/abap-api-tools/src/ts/backend.ts
index 5d38332f..0d0ab1a3 100644
--- a/abap-api-tools/src/ts/backend.ts
+++ b/abap-api-tools/src/ts/backend.ts
@@ -37,7 +37,7 @@ export type ParameterType = {
 };
 type yamlParameters = Record<string, Record<string, ParameterType>>;
 
-export type FieldText = {
+export type FieldTextType = {
   FIELDTEXT?: string;
   REPTEXT?: string;
   SCRTEXT_S?: string;
@@ -67,13 +67,15 @@ export type FieldType = {
     MEMORYID?: string;
     SHLP?: string;
   };
-  text: FieldText;
+  text: FieldTextType;
 };
 
 export type StructureType = Record<string, FieldType>;
 
 export type yamlFields = Record<string, FieldType | StructureType>;
 
+export type yamlTexts = Record<string, Record<string, string | FieldTextType>>;
+
 // AbapObject typings
 
 type FieldValuesHelpType = {
@@ -173,10 +175,10 @@ export class Backend {
   private SPRAS: string;
   private Helps: Helps;
   private Stat: Stat;
+  private Texts = {} as yamlTexts;
 
   private client: Client;
 
-  // tslint:disable:no-empty
   constructor(api_name: string, argv: Arguments) {
     this.argv = argv;
     this.api_name = api_name;
@@ -223,7 +225,7 @@ export class Backend {
     }
   }
 
-  getDfiesText(dfies: RfcStructure): FieldText {
+  getDfiesText(dfies: RfcStructure): FieldTextType {
     const TEXT_FIELDS = [
       "FIELDTEXT",
       "REPTEXT",
@@ -231,10 +233,12 @@ export class Backend {
       "SCRTEXT_M",
       "SCRTEXT_L",
     ];
-    const text: FieldText = {};
+    const text: FieldTextType = {};
     TEXT_FIELDS.forEach((t) => {
       if ((dfies[t] as string).length > 0) text[t] = dfies[t];
     });
+    // todo: remove this eventually
+    if (!text.FIELDTEXT) text.FIELDTEXT = "No field text";
     return text;
   }
 
@@ -498,6 +502,31 @@ export class Backend {
           if ((field.INTTYPE as string).trim() && !field[".INCLUDE"]) {
             this.alpha.field(field.FIELDNAME as string);
             result[field.FIELDNAME as string] = await this.getField(field);
+
+            // Field texts -> Texts
+            const tkey = JSON.stringify({
+              t: param.TABNAME as string,
+              f: field.FIELDNAME as string,
+            });
+            const texts = result[field.FIELDNAME as string].text;
+            if (this.argv.textOnly) {
+              for (const [k, v] of Object.entries(this.Texts)) {
+                if (v._id === tkey) {
+                  this.Texts[k][this.argv.lang] = texts.FIELDTEXT as string;
+                  this.Texts[k].short[this.argv.lang] = texts;
+                  break;
+                }
+              }
+              // if not found, the same text already added by another field
+            } else {
+              if (!this.Texts[texts.FIELDTEXT as string]) {
+                this.Texts[texts.FIELDTEXT as string] = {
+                  _id: tkey,
+                  [this.argv.lang]: texts.FIELDTEXT as string,
+                  short: { [this.argv.lang]: texts },
+                };
+              }
+            }
           }
         }
         return result;
@@ -522,7 +551,19 @@ export class Backend {
   }
 
   async parse(): Promise<AbapObject> {
-    this.annotations_clean();
+    if (!this.argv.textOnly) {
+      this.annotations_clean();
+    } else {
+      // Texts
+      if (this.argv.textOnly) {
+        const folder_yaml = this.api_name
+          ? path.join(this.argv.output, this.api_name, "yaml")
+          : path.join(this.argv.output, "yaml");
+        this.Texts = yamlLoad(
+          path.join(folder_yaml, "texts.yaml")
+        ) as yamlTexts;
+      }
+    }
 
     log.info(
       `\napi ${this.argv.dest} ${chalk.bold(this.api_name)} language: ${
@@ -561,8 +602,8 @@ export class Backend {
         continue;
       }
       // more intuitive names
-      p.functionName = p["FUNCNAME"].trim() as string;
-      p.paramName = p["PARAMETER"].trim() as string;
+      p.functionName = (p["FUNCNAME"] as string).trim();
+      p.paramName = (p["PARAMETER"] as string).trim();
 
       // Trim, jusr for any case
       p.FIELDNAME.trim();
@@ -603,7 +644,6 @@ export class Backend {
     }
 
     // Sort by rfm / parameter class / required/optional / parameter type and name
-
     (R["PARAMETERS"] as RfcTable).sort((a: RfcStructure, b: RfcStructure) => {
       const PClass = ["I", "C", "T", "E", "X"];
       const PType = [
@@ -658,6 +698,25 @@ export class Backend {
       // stat
       this.Stat[functionName][p.paramType as string]++;
 
+      // Parameter text -> Texts
+      const tkey = JSON.stringify({ r: p.functionName, p: p.paramName });
+      if (this.argv.textOnly) {
+        for (const [k, v] of Object.entries(this.Texts)) {
+          if (v._id === tkey) {
+            this.Texts[k][this.argv.lang] = p.PARAMTEXT as string;
+            break;
+          }
+        }
+        // if not found, the same text already added by another parameter
+      } else {
+        if (!this.Texts[p.PARAMTEXT as string]) {
+          this.Texts[p.PARAMTEXT as string] = {
+            _id: tkey,
+            [this.argv.lang]: p.PARAMTEXT as string,
+          };
+        }
+      }
+
       //
       // dfies
       //
@@ -750,20 +809,26 @@ export class Backend {
       usage: usage,
     };
 
-    if (this.argv._[0] === Command.get) {
-      this.annotations_write(abap);
+    if (this.argv.cmd === Command.get) {
+      this.annotations_write(abap, this.Texts, this.argv.textOnly);
     }
 
     return abap;
   }
 
-  annotations_write(abap: AbapObject): void {
+  annotations_write(
+    abap: AbapObject,
+    texts: yamlTexts,
+    textOnly: boolean
+  ): void {
     const folder_root: string = path.join(
       this.argv.output as string,
       this.api_name
     );
     const folder_yaml: string = path.join(folder_root, "yaml");
-    log.debug(`Annotations save ${folder_yaml}`);
+    log.debug(
+      `Annotations save ${folder_yaml} ${textOnly ? "only texts" : ""}`
+    );
 
     if (!fs.existsSync(folder_root)) {
       fs.mkdirSync(folder_root);
@@ -771,15 +836,25 @@ export class Backend {
     if (!fs.existsSync(folder_yaml)) {
       fs.mkdirSync(folder_yaml);
     }
-
-    yamlSave(`${folder_yaml}/alpha.yaml`, abap.alpha, { sortKeys: true });
-    yamlSave(`${folder_yaml}/parameters.yaml`, abap.parameters);
-    yamlSave(`${folder_yaml}/fields.yaml`, abap.fields);
-    if (abap.helps) {
-      yamlSave(`${folder_yaml}/helps.yaml`, abap.helps, { sortKeys: true });
+    if (!textOnly) {
+      yamlSave(path.join(folder_yaml, "alpha.yaml"), abap.alpha, {
+        sortKeys: true,
+      });
+      yamlSave(path.join(folder_yaml, "parameters.yaml"), abap.parameters);
+      yamlSave(path.join(folder_yaml, "fields.yaml"), abap.fields);
+      if (abap.helps) {
+        yamlSave(path.join(folder_yaml, "helps.yaml"), abap.helps, {
+          sortKeys: true,
+        });
+      }
+      yamlSave(path.join(folder_yaml, "stat.yaml"), abap.stat);
+      yamlSave(path.join(folder_yaml, "usage.yaml"), abap.usage);
+    }
+    if (!isEmpty(texts)) {
+      yamlSave(path.join(folder_yaml, "texts.yaml"), texts, {
+        sortKeys: true,
+      });
     }
-    yamlSave(`${folder_yaml}/stat.yaml`, abap.stat);
-    yamlSave(`${folder_yaml}/usage.yaml`, abap.usage);
   }
 
   annotations_clean(): void {
@@ -790,6 +865,7 @@ export class Backend {
     );
 
     log.debug(`Annotations clean ${folder_yaml}`);
+
     for (const fileName of [
       "parameters",
       "fields",
@@ -797,8 +873,9 @@ export class Backend {
       "stat",
       "alpha",
       "usage",
+      "texts",
     ]) {
-      deleteFile(`${folder_yaml}/${fileName}.yaml`);
+      deleteFile(path.join(folder_yaml, `${fileName}.yaml`));
     }
   }
 }
@@ -814,8 +891,10 @@ export function annotations_read(
   log.debug(`reading annotations for: ${api_name} from ${folder_yaml}`);
 
   return {
-    parameters: yamlLoad(`${folder_yaml}/parameters.yaml`) as yamlParameters,
-    fields: yamlLoad(`${folder_yaml}/fields.yaml`) as yamlFields,
-    stat: yamlLoad(`${folder_yaml}/stat.yaml`) as Stat,
+    parameters: yamlLoad(
+      path.join(folder_yaml, "parameters.yaml")
+    ) as yamlParameters,
+    fields: yamlLoad(path.join(folder_yaml, "fields.yaml")) as yamlFields,
+    stat: yamlLoad(path.join(folder_yaml, "stat.yaml")) as Stat,
   };
 }