Skip to content

Commit 782b0f9

Browse files
skapur12dpwatrous
authored andcommitted
Integrate localized strings into web and desktop app (#2730)
* Added fake localizer * Switch desktop app to use new translations * Add script to combine translated files by language * Add global build-translations command * Update comments and docs accordingly with changes * Run merge-translations script on Azure DevOps path * Add localization for Batch Explorer web version * Generate translations for Create Account buttons * Add basic i18n support for web package * Rename StandardLocalizer to BrowserLocalizer * Add merge translations script for web and desktop * Remove powershell merge translations script * Add localization support for desktop app * Copy translations to web too and not just desktop * Add translations for playground buttons * Gitignore generated localization files * Update localization docs with setup instructions * Add package translations to mergeTranslations * Optimize package file creation and fix unit test * Update Electron app localization unit tests * Address all feedback on PR * Fix desktop localizer, translation function, tests * Fix Prettier issue with account yml file * Update client translations unit test accordingly * Add http-localizer unit test and minor fixes * Remove CustomGlobal and simplify navigator object * Add getLocale function to each localizer --------- Co-authored-by: David Watrous <509299+dpwatrous@users.noreply.github.com>
1 parent 0c99872 commit 782b0f9

44 files changed

Lines changed: 600 additions & 160 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ Localize/out/
1313
/packages/playground/resources/i18n/*
1414
/packages/react/resources/i18n/*
1515
/packages/service/resources/i18n/*
16-
/util/bux/resources/i18n/json
16+
web/dev-server/resources/i18n/*
17+
desktop/resources/i18n/*

.vsts/linux/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ steps:
1717
echo "No changes. Nothing to do"
1818
exit 0
1919
else
20-
echo "Text changes detected: $changed. Pipeline failed."
20+
echo "Text changes detected: $changed. Pipeline failed. Please run 'npm run build' on your local machine and push the generated files."
2121
exit 1
2222
fi
2323
workingDirectory: desktop

Localize/LocProject.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
},
1818
{
1919
"SourceFile": "desktop\\i18n\\resources.resjson"
20+
},
21+
{
22+
"SourceFile": "web\\i18n\\resources.resjson"
2023
}
2124
]
2225
}

Localize/copy-translations.ps1

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@ if ($artifactsPath -eq "") {
1212
$sourceRoot = $artifactsPath
1313
}
1414

15-
$packageNames = @("common", "service", "playground", "react")
15+
$packageNames = @("common", "service", "playground", "react", "web", "desktop")
1616

1717
# Get language directories
1818
$languageDirs = Get-ChildItem -Path $sourceRoot -Directory
1919

20-
# If this script is run locally, in addition to the resjson files, it adds a json directory containing json files for local development
21-
# If script is run on ADO, it only adds the resjson files used in production
22-
2320
# Strip out resjson comments and resjson-specific formatting before writing the result to json file
2421
function Convert-ResjsonToJson {
2522
param (
@@ -43,63 +40,60 @@ function Convert-ResjsonToJson {
4340
Set-Content -Path $targetPath -Value $cleanedJsonContent
4441
}
4542

46-
Write-Host "Copying translation files"
47-
48-
foreach ($languageDir in $languageDirs) {
49-
$languageId = $languageDir.Name
50-
Write-Verbose "Processing language: $languageId"
43+
function Copy-Resources {
44+
param (
45+
[string]$packageName,
46+
[string]$languageDirFullName,
47+
[string]$languageId
48+
)
5149

52-
# Process package directories
53-
foreach ($packageName in $packageNames) {
54-
$sourcePath = Join-Path $($languageDir.FullName) "packages/$packageName/i18n/resources.resjson"
55-
$targetDir = (Join-Path $scriptDir ".." -Resolve) | Join-Path -ChildPath "packages/$packageName/resources/i18n/resjson"
50+
if ($packageName -eq "web" -or $packageName -eq "desktop") {
51+
$sourcePath = Join-Path $languageDirFullName "$packageName/i18n/resources.resjson"
52+
if ($packageName -eq "web") {
53+
$targetDir = Join-Path $scriptDir "../$packageName/dev-server/resources/i18n"
54+
} else {
55+
$targetDir = Join-Path $scriptDir "../$packageName/resources/i18n"
56+
}
57+
$targetPath = Join-Path $targetDir "resources.$languageId.json"
58+
} else {
59+
$sourcePath = Join-Path $languageDirFullName "packages/$packageName/i18n/resources.resjson"
60+
$targetDir = Join-Path $scriptDir ".." -Resolve | Join-Path -ChildPath "packages/$packageName/resources/i18n/resjson"
5661
$targetPath = Join-Path $targetDir "resources.$languageId.resjson"
62+
}
5763

58-
Write-Verbose "Checking source path: $sourcePath"
59-
if (Test-Path $sourcePath) {
60-
Write-Verbose "Source path exists, preparing target directory"
61-
if (-not (Test-Path $targetDir)) {
62-
New-Item -ItemType Directory -Force -Path $targetDir > $null
63-
}
64+
Write-Verbose "Checking $packageName source path: $sourcePath"
65+
if (Test-Path $sourcePath) {
66+
Write-Verbose "$packageName source path exists, preparing target directory"
67+
if (-not (Test-Path $targetDir)) {
68+
New-Item -ItemType Directory -Force -Path $targetDir > $null
69+
}
6470

65-
Write-Verbose "Copying file from $sourcePath to $targetPath"
66-
Copy-Item -Path $sourcePath -Destination $targetPath
71+
Write-Verbose "Copying file from $sourcePath to $targetPath"
72+
Copy-Item -Path $sourcePath -Destination $targetPath
6773

74+
if ($packageName -ne "web" -and $packageName -ne "desktop") {
6875
$jsonTargetDir = $targetDir.Replace("resjson", "json")
69-
$jsonTargetPath = $targetPath.Replace("resjson", "json")
70-
76+
$jsonTargetPath = Join-Path $jsonTargetDir "resources.$languageId.json"
7177
if (-not (Test-Path $jsonTargetDir)) {
7278
New-Item -ItemType Directory -Force -Path $jsonTargetDir > $null
7379
}
74-
75-
Write-Verbose "Converting resjson to json: $jsonTargetPath"
76-
Convert-ResjsonToJson -sourcePath $sourcePath -targetPath $jsonTargetPath
80+
} else {
81+
$jsonTargetPath = $targetPath
7782
}
78-
}
7983

80-
# Handle the desktop directory exception
81-
$desktopSource = Join-Path $($languageDir.FullName) "desktop/i18n/resources.resjson"
82-
$desktopTargetDir = (Join-Path $scriptDir "../desktop/resources/i18n/resjson")
83-
$desktopTarget = Join-Path $desktopTargetDir "resources.$languageId.resjson"
84-
85-
Write-Verbose "Checking desktop source path: $desktopSource"
86-
if (Test-Path $desktopSource) {
87-
Write-Verbose "Desktop source path exists, preparing target directory"
88-
if (-not (Test-Path $desktopTargetDir)) {
89-
New-Item -ItemType Directory -Force -Path $desktopTargetDir > $null
90-
}
91-
92-
Write-Verbose "Copying file from $desktopSource to $desktopTarget"
93-
Copy-Item -Path $desktopSource -Destination $desktopTarget
84+
Write-Verbose "Converting $packageName resjson to json: $jsonTargetPath"
85+
Convert-ResjsonToJson -sourcePath $sourcePath -targetPath $jsonTargetPath
86+
}
87+
}
9488

95-
$jsonDesktopTargetDir = $desktopTargetDir.Replace("resjson", "json")
96-
$jsonDesktopTarget = $desktopTarget.Replace("resjson", "json")
89+
Write-Host "Copying translation files"
9790

98-
if (-not (Test-Path $jsonDesktopTargetDir)) {
99-
New-Item -ItemType Directory -Force -Path $jsonDesktopTargetDir > $null
100-
}
91+
foreach ($languageDir in $languageDirs) {
92+
$languageId = $languageDir.Name
93+
Write-Verbose "Processing language: $languageId"
10194

102-
Write-Verbose "Converting desktop resjson to json: $jsonDesktopTarget"
103-
Convert-ResjsonToJson -sourcePath $desktopSource -targetPath $jsonDesktopTarget
95+
# Copy files to each of the package directories
96+
foreach ($packageName in $packageNames) {
97+
Copy-Resources -packageName $packageName -languageDirFullName $($languageDir.FullName) -languageId $languageId
10498
}
10599
}

desktop/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"build:package": "npm run build:prod && npm run build-python && npm run package",
5151
"build:prod": "cross-env NODE_ENV=production npm run build",
5252
"build:test": "npm run build && npm run test",
53-
"build-translations": "bux build-translations --src src --dest i18n",
53+
"build-translations": "bux build-translations --src src --dest i18n --outputPath resources/i18n",
5454
"watch": "npm run webpack -- --watch --progress --profile --colors --display-error-details --display-cached",
5555
"electron": "electron build/client/main.js",
5656
"electron:prod": "cross-env NODE_ENV=production electron build/client/main.js",
@@ -76,6 +76,7 @@
7676
"workspace:build:package": "npm run build:package",
7777
"workspace:build:prod": "npm run build:prod",
7878
"workspace:build:test": "npm run build:test",
79+
"workspace:build-translations": "npm run build-translations",
7980
"workspace:clean": "npm run clean",
8081
"workspace:launch:desktop": "npm run dev",
8182
"workspace:lint": "npm run lint",

desktop/src/app/app.component.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { BrowserDependencyName } from "@batch/ui-react";
1616
import { StorageAccountServiceImpl, SubscriptionServiceImpl } from "@batch/ui-service";
1717
import { registerIcons } from "app/config";
1818
import {
19+
AppTranslationsLoaderService,
1920
AuthorizationHttpService,
2021
AuthService,
2122
BatchAccountService,
@@ -31,9 +32,9 @@ import { Environment } from "common/constants";
3132
import { Subject, combineLatest } from "rxjs";
3233
import { takeUntil } from "rxjs/operators";
3334
import { DefaultBrowserEnvironment } from "@batch/ui-react/lib/environment";
34-
import {StandardLocalizer} from "@batch/ui-common/lib/localization/standard-localizer";
3535
import { LiveLocationService } from "@batch/ui-service/lib/location";
3636
import { LiveResourceGroupService } from "@batch/ui-service/lib/resource-group";
37+
import { DesktopLocalizer } from "./localizer/desktop-localizer";
3738

3839
@Component({
3940
selector: "bl-app",
@@ -68,17 +69,18 @@ export class AppComponent implements OnInit, OnDestroy {
6869
private ncjTemplateService: NcjTemplateService,
6970
private predefinedFormulaService: PredefinedFormulaService,
7071
private workspaceService: WorkspaceService,
72+
private translationsLoaderService: AppTranslationsLoaderService
7173
) {
72-
// Initialize shared component lib environment
73-
initEnvironment(new DefaultBrowserEnvironment(
74+
// Initialize shared component lib environment
75+
initEnvironment(new DefaultBrowserEnvironment(
7476
{
7577
mode: ENV === Environment.prod ? EnvironmentMode.Production : EnvironmentMode.Development
7678
},
7779
{
7880
[DependencyName.Clock]: () => new StandardClock(),
7981
// TODO: Create an adapter which hooks up to the desktop logger
8082
[DependencyName.LoggerFactory]: () => createConsoleLogger,
81-
[DependencyName.Localizer]: () => new StandardLocalizer(),
83+
[DependencyName.Localizer]: () => new DesktopLocalizer(this.translationsLoaderService),
8284
[DependencyName.HttpClient]:
8385
() => new BatchExplorerHttpClient(authService),
8486
[BrowserDependencyName.LocationService]: () =>
@@ -94,7 +96,7 @@ export class AppComponent implements OnInit, OnDestroy {
9496
[BrowserDependencyName.FormLayoutProvider]:
9597
() => new DefaultFormLayoutProvider(),
9698
}
97-
));
99+
));
98100

99101
this.telemetryService.init(remote.getCurrentWindow().TELEMETRY_ENABLED);
100102
this._initWorkspaces();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { app } from 'electron';
2+
import { Localizer } from "@batch/ui-common/lib/localization";
3+
import { AppTranslationsLoaderService } from "app/services";
4+
5+
export class DesktopLocalizer implements Localizer {
6+
constructor(
7+
private translationsLoaderService: AppTranslationsLoaderService
8+
) { }
9+
10+
translate(message: string): string {
11+
const translations = this.translationsLoaderService.translations;
12+
if (!translations) {
13+
throw new Error("Translation strings are not loaded: " + message);
14+
}
15+
16+
const translation = translations.get(message);
17+
18+
if (translation != null) {
19+
return translation;
20+
} else {
21+
return message;
22+
}
23+
}
24+
25+
getLocale(): string {
26+
if (process.type === 'renderer') {
27+
return navigator.language;
28+
} else {
29+
return app.getLocale();
30+
}
31+
}
32+
}

desktop/src/client/core/i18n/client-translations-loader.service.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ describe("ClientTranslationsLoaderService", () => {
5858
it("it only loads the english translations file when locale is english", async () => {
5959
localService.locale = "en";
6060
await loader.load();
61-
expect(fsSpy.readFile).toHaveBeenCalledTimes(1);
62-
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "i18n-deprecated/resources.en.json"));
61+
expect(fsSpy.readFile).toHaveBeenCalledTimes(2);
62+
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "./resources/i18n/resources.en.json"));
6363

6464
expect(loader.translations.get("foo.banana")).toEqual("Banana");
6565
expect(loader.translations.get("foo.potato")).toEqual("Potato");
@@ -69,8 +69,8 @@ describe("ClientTranslationsLoaderService", () => {
6969
localService.locale = "fr";
7070
await loader.load();
7171
expect(fsSpy.readFile).toHaveBeenCalledTimes(2);
72-
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "i18n-deprecated/resources.en.json"));
73-
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "i18n-deprecated/resources.fr.json"));
72+
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "./resources/i18n/resources.en.json"));
73+
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "./resources/i18n/resources.fr.json"));
7474

7575
expect(loader.translations.get("foo.potato")).toEqual("Pomme de terre");
7676
expect(loader.translations.get("foo.banana")).toEqual("Banana", "Use english translation when not present");
@@ -90,7 +90,7 @@ describe("ClientTranslationsLoaderService", () => {
9090
localService.locale = "en";
9191
await loader.load();
9292
expect(devTranslationLoaderSpy.load).toHaveBeenCalledTimes(1);
93-
expect(fsSpy.readFile).not.toHaveBeenCalled();
93+
expect(fsSpy.readFile).toHaveBeenCalledTimes(1);
9494

9595
expect(loader.translations.get("foo.banana")).toEqual("Banana");
9696
expect(loader.translations.get("foo.potato")).toEqual("Potato");
@@ -102,7 +102,7 @@ describe("ClientTranslationsLoaderService", () => {
102102
await loader.load();
103103
expect(devTranslationLoaderSpy.load).toHaveBeenCalledTimes(1);
104104
expect(fsSpy.readFile).toHaveBeenCalledTimes(1);
105-
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "i18n-deprecated/resources.fr.json"));
105+
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "./resources/i18n/resources.fr.json"));
106106

107107
expect(loader.translations.get("foo.potato")).toEqual("Pomme de terre");
108108
expect(loader.translations.get("foo.banana")).toEqual("Banana", "Use english translation when not present");

desktop/src/client/core/i18n/client-translations-loader.service.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class ClientTranslationsLoaderService extends TranslationsLoaderService {
4141
}
4242

4343
private async _loadProductionTranslations() {
44-
const englishTranslationFile = path.join(ClientConstants.resourcesFolder, "./i18n-deprecated/resources.en.json");
44+
const englishTranslationFile = path.join(ClientConstants.resourcesFolder, "./resources/i18n/resources.en.json");
4545
await this._loadProductionTranslationFile(englishTranslationFile);
4646
await this._loadLocaleTranslations();
4747
}
@@ -51,8 +51,7 @@ export class ClientTranslationsLoaderService extends TranslationsLoaderService {
5151
*/
5252
private async _loadLocaleTranslations() {
5353
const locale = this.localeService.locale;
54-
if (locale === Locale.English) { return; }
55-
const localeTranslationFile = path.join(ClientConstants.resourcesFolder, `./i18n-deprecated/resources.${locale}.json`);
54+
const localeTranslationFile = path.join(ClientConstants.resourcesFolder, `./resources/i18n/resources.${locale}.json`);
5655
if (await this.fs.exists(localeTranslationFile)) {
5756
await this._loadProductionTranslationFile(localeTranslationFile);
5857
} else {

docs/build-localization-locally.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
# Building Localization on Local Machine
22

3-
## Building the English File
3+
## Building the English File (Any Machine)
44

5-
To build the latest English translation resjson file from the YAML files:
5+
To build the latest English translation file from the YAML files:
66

7-
* Run `npm run build` to build translations for the entire repository
8-
* Run `npm run build-translations` in any package directory (desktop, packages/*) to build translations for a specific package
7+
* Run `npm run build-translations`
98

10-
The output will be in `{packageName}/i18n/resources.resjson`
9+
* `web/dev-server/resources/i18n/resources.en.json` contains web English strings (web + all packages)
10+
* `desktop/resources/i18n/resources.en.json` contains desktop English strings (desktop + all packages)
1111

1212
## Building Translations Files for Other Languages (Windows-Only)
1313

1414
To build the localization translations for all languages besides English:
1515

16-
* Follow the steps above first
16+
* Follow the step above first (build the English file)
1717
* Install the latest, recommended version of nuget.exe from <https://www.nuget.org/downloads> at C:\Users\{userName}, for instance.
1818
* Navigate to the root of the repository
1919
* Run `npm run loc:restore` to install all dependencies
20-
* Run `npm run loc:build` to build the translations and move them to their correct directories
21-
* If needed, run `npm run clean` to clear out all previously built translation files
20+
* Run `npm run loc:build` to build the translations, move them to the package directories, and combine them altogether in one directory
21+
* Run `npm run build-translations` to build the full, compiled translations for the web and desktop packages
2222

23-
The output will be in `{packageName}/resources/i18n`
24-
25-
* `{packageName}/resources/i18n/resjson` contains RESJSON translations
26-
* `{packageName}/resources/i18n/json` contains JSON translations (RESJSON syntax and comments have been stripped out)
23+
* `web/dev-server/resources/i18n` contains web translations (web + all packages)
24+
* `desktop/resources/i18n` contains desktop translations (desktop + all packages)

0 commit comments

Comments
 (0)