зеркало из https://github.com/Azure/BatchExplorer.git
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>
This commit is contained in:
Родитель
0c99872d82
Коммит
782b0f9fde
|
@ -13,4 +13,5 @@ Localize/out/
|
|||
/packages/playground/resources/i18n/*
|
||||
/packages/react/resources/i18n/*
|
||||
/packages/service/resources/i18n/*
|
||||
/util/bux/resources/i18n/json
|
||||
web/dev-server/resources/i18n/*
|
||||
desktop/resources/i18n/*
|
||||
|
|
|
@ -17,7 +17,7 @@ steps:
|
|||
echo "No changes. Nothing to do"
|
||||
exit 0
|
||||
else
|
||||
echo "Text changes detected: $changed. Pipeline failed."
|
||||
echo "Text changes detected: $changed. Pipeline failed. Please run 'npm run build' on your local machine and push the generated files."
|
||||
exit 1
|
||||
fi
|
||||
workingDirectory: desktop
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
},
|
||||
{
|
||||
"SourceFile": "desktop\\i18n\\resources.resjson"
|
||||
},
|
||||
{
|
||||
"SourceFile": "web\\i18n\\resources.resjson"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -12,14 +12,11 @@ if ($artifactsPath -eq "") {
|
|||
$sourceRoot = $artifactsPath
|
||||
}
|
||||
|
||||
$packageNames = @("common", "service", "playground", "react")
|
||||
$packageNames = @("common", "service", "playground", "react", "web", "desktop")
|
||||
|
||||
# Get language directories
|
||||
$languageDirs = Get-ChildItem -Path $sourceRoot -Directory
|
||||
|
||||
# If this script is run locally, in addition to the resjson files, it adds a json directory containing json files for local development
|
||||
# If script is run on ADO, it only adds the resjson files used in production
|
||||
|
||||
# Strip out resjson comments and resjson-specific formatting before writing the result to json file
|
||||
function Convert-ResjsonToJson {
|
||||
param (
|
||||
|
@ -43,63 +40,60 @@ function Convert-ResjsonToJson {
|
|||
Set-Content -Path $targetPath -Value $cleanedJsonContent
|
||||
}
|
||||
|
||||
function Copy-Resources {
|
||||
param (
|
||||
[string]$packageName,
|
||||
[string]$languageDirFullName,
|
||||
[string]$languageId
|
||||
)
|
||||
|
||||
if ($packageName -eq "web" -or $packageName -eq "desktop") {
|
||||
$sourcePath = Join-Path $languageDirFullName "$packageName/i18n/resources.resjson"
|
||||
if ($packageName -eq "web") {
|
||||
$targetDir = Join-Path $scriptDir "../$packageName/dev-server/resources/i18n"
|
||||
} else {
|
||||
$targetDir = Join-Path $scriptDir "../$packageName/resources/i18n"
|
||||
}
|
||||
$targetPath = Join-Path $targetDir "resources.$languageId.json"
|
||||
} else {
|
||||
$sourcePath = Join-Path $languageDirFullName "packages/$packageName/i18n/resources.resjson"
|
||||
$targetDir = Join-Path $scriptDir ".." -Resolve | Join-Path -ChildPath "packages/$packageName/resources/i18n/resjson"
|
||||
$targetPath = Join-Path $targetDir "resources.$languageId.resjson"
|
||||
}
|
||||
|
||||
Write-Verbose "Checking $packageName source path: $sourcePath"
|
||||
if (Test-Path $sourcePath) {
|
||||
Write-Verbose "$packageName source path exists, preparing target directory"
|
||||
if (-not (Test-Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $targetDir > $null
|
||||
}
|
||||
|
||||
Write-Verbose "Copying file from $sourcePath to $targetPath"
|
||||
Copy-Item -Path $sourcePath -Destination $targetPath
|
||||
|
||||
if ($packageName -ne "web" -and $packageName -ne "desktop") {
|
||||
$jsonTargetDir = $targetDir.Replace("resjson", "json")
|
||||
$jsonTargetPath = Join-Path $jsonTargetDir "resources.$languageId.json"
|
||||
if (-not (Test-Path $jsonTargetDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $jsonTargetDir > $null
|
||||
}
|
||||
} else {
|
||||
$jsonTargetPath = $targetPath
|
||||
}
|
||||
|
||||
Write-Verbose "Converting $packageName resjson to json: $jsonTargetPath"
|
||||
Convert-ResjsonToJson -sourcePath $sourcePath -targetPath $jsonTargetPath
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Copying translation files"
|
||||
|
||||
foreach ($languageDir in $languageDirs) {
|
||||
$languageId = $languageDir.Name
|
||||
Write-Verbose "Processing language: $languageId"
|
||||
|
||||
# Process package directories
|
||||
# Copy files to each of the package directories
|
||||
foreach ($packageName in $packageNames) {
|
||||
$sourcePath = Join-Path $($languageDir.FullName) "packages/$packageName/i18n/resources.resjson"
|
||||
$targetDir = (Join-Path $scriptDir ".." -Resolve) | Join-Path -ChildPath "packages/$packageName/resources/i18n/resjson"
|
||||
$targetPath = Join-Path $targetDir "resources.$languageId.resjson"
|
||||
|
||||
Write-Verbose "Checking source path: $sourcePath"
|
||||
if (Test-Path $sourcePath) {
|
||||
Write-Verbose "Source path exists, preparing target directory"
|
||||
if (-not (Test-Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $targetDir > $null
|
||||
}
|
||||
|
||||
Write-Verbose "Copying file from $sourcePath to $targetPath"
|
||||
Copy-Item -Path $sourcePath -Destination $targetPath
|
||||
|
||||
$jsonTargetDir = $targetDir.Replace("resjson", "json")
|
||||
$jsonTargetPath = $targetPath.Replace("resjson", "json")
|
||||
|
||||
if (-not (Test-Path $jsonTargetDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $jsonTargetDir > $null
|
||||
}
|
||||
|
||||
Write-Verbose "Converting resjson to json: $jsonTargetPath"
|
||||
Convert-ResjsonToJson -sourcePath $sourcePath -targetPath $jsonTargetPath
|
||||
}
|
||||
}
|
||||
|
||||
# Handle the desktop directory exception
|
||||
$desktopSource = Join-Path $($languageDir.FullName) "desktop/i18n/resources.resjson"
|
||||
$desktopTargetDir = (Join-Path $scriptDir "../desktop/resources/i18n/resjson")
|
||||
$desktopTarget = Join-Path $desktopTargetDir "resources.$languageId.resjson"
|
||||
|
||||
Write-Verbose "Checking desktop source path: $desktopSource"
|
||||
if (Test-Path $desktopSource) {
|
||||
Write-Verbose "Desktop source path exists, preparing target directory"
|
||||
if (-not (Test-Path $desktopTargetDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $desktopTargetDir > $null
|
||||
}
|
||||
|
||||
Write-Verbose "Copying file from $desktopSource to $desktopTarget"
|
||||
Copy-Item -Path $desktopSource -Destination $desktopTarget
|
||||
|
||||
$jsonDesktopTargetDir = $desktopTargetDir.Replace("resjson", "json")
|
||||
$jsonDesktopTarget = $desktopTarget.Replace("resjson", "json")
|
||||
|
||||
if (-not (Test-Path $jsonDesktopTargetDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $jsonDesktopTargetDir > $null
|
||||
}
|
||||
|
||||
Write-Verbose "Converting desktop resjson to json: $jsonDesktopTarget"
|
||||
Convert-ResjsonToJson -sourcePath $desktopSource -targetPath $jsonDesktopTarget
|
||||
Copy-Resources -packageName $packageName -languageDirFullName $($languageDir.FullName) -languageId $languageId
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
"build:package": "npm run build:prod && npm run build-python && npm run package",
|
||||
"build:prod": "cross-env NODE_ENV=production npm run build",
|
||||
"build:test": "npm run build && npm run test",
|
||||
"build-translations": "bux build-translations --src src --dest i18n",
|
||||
"build-translations": "bux build-translations --src src --dest i18n --outputPath resources/i18n",
|
||||
"watch": "npm run webpack -- --watch --progress --profile --colors --display-error-details --display-cached",
|
||||
"electron": "electron build/client/main.js",
|
||||
"electron:prod": "cross-env NODE_ENV=production electron build/client/main.js",
|
||||
|
@ -76,6 +76,7 @@
|
|||
"workspace:build:package": "npm run build:package",
|
||||
"workspace:build:prod": "npm run build:prod",
|
||||
"workspace:build:test": "npm run build:test",
|
||||
"workspace:build-translations": "npm run build-translations",
|
||||
"workspace:clean": "npm run clean",
|
||||
"workspace:launch:desktop": "npm run dev",
|
||||
"workspace:lint": "npm run lint",
|
||||
|
|
|
@ -16,6 +16,7 @@ import { BrowserDependencyName } from "@batch/ui-react";
|
|||
import { StorageAccountServiceImpl, SubscriptionServiceImpl } from "@batch/ui-service";
|
||||
import { registerIcons } from "app/config";
|
||||
import {
|
||||
AppTranslationsLoaderService,
|
||||
AuthorizationHttpService,
|
||||
AuthService,
|
||||
BatchAccountService,
|
||||
|
@ -31,9 +32,9 @@ import { Environment } from "common/constants";
|
|||
import { Subject, combineLatest } from "rxjs";
|
||||
import { takeUntil } from "rxjs/operators";
|
||||
import { DefaultBrowserEnvironment } from "@batch/ui-react/lib/environment";
|
||||
import {StandardLocalizer} from "@batch/ui-common/lib/localization/standard-localizer";
|
||||
import { LiveLocationService } from "@batch/ui-service/lib/location";
|
||||
import { LiveResourceGroupService } from "@batch/ui-service/lib/resource-group";
|
||||
import { DesktopLocalizer } from "./localizer/desktop-localizer";
|
||||
|
||||
@Component({
|
||||
selector: "bl-app",
|
||||
|
@ -68,9 +69,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
private ncjTemplateService: NcjTemplateService,
|
||||
private predefinedFormulaService: PredefinedFormulaService,
|
||||
private workspaceService: WorkspaceService,
|
||||
private translationsLoaderService: AppTranslationsLoaderService
|
||||
) {
|
||||
// Initialize shared component lib environment
|
||||
initEnvironment(new DefaultBrowserEnvironment(
|
||||
// Initialize shared component lib environment
|
||||
initEnvironment(new DefaultBrowserEnvironment(
|
||||
{
|
||||
mode: ENV === Environment.prod ? EnvironmentMode.Production : EnvironmentMode.Development
|
||||
},
|
||||
|
@ -78,7 +80,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
[DependencyName.Clock]: () => new StandardClock(),
|
||||
// TODO: Create an adapter which hooks up to the desktop logger
|
||||
[DependencyName.LoggerFactory]: () => createConsoleLogger,
|
||||
[DependencyName.Localizer]: () => new StandardLocalizer(),
|
||||
[DependencyName.Localizer]: () => new DesktopLocalizer(this.translationsLoaderService),
|
||||
[DependencyName.HttpClient]:
|
||||
() => new BatchExplorerHttpClient(authService),
|
||||
[BrowserDependencyName.LocationService]: () =>
|
||||
|
@ -94,7 +96,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
[BrowserDependencyName.FormLayoutProvider]:
|
||||
() => new DefaultFormLayoutProvider(),
|
||||
}
|
||||
));
|
||||
));
|
||||
|
||||
this.telemetryService.init(remote.getCurrentWindow().TELEMETRY_ENABLED);
|
||||
this._initWorkspaces();
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { app } from 'electron';
|
||||
import { Localizer } from "@batch/ui-common/lib/localization";
|
||||
import { AppTranslationsLoaderService } from "app/services";
|
||||
|
||||
export class DesktopLocalizer implements Localizer {
|
||||
constructor(
|
||||
private translationsLoaderService: AppTranslationsLoaderService
|
||||
) { }
|
||||
|
||||
translate(message: string): string {
|
||||
const translations = this.translationsLoaderService.translations;
|
||||
if (!translations) {
|
||||
throw new Error("Translation strings are not loaded: " + message);
|
||||
}
|
||||
|
||||
const translation = translations.get(message);
|
||||
|
||||
if (translation != null) {
|
||||
return translation;
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
getLocale(): string {
|
||||
if (process.type === 'renderer') {
|
||||
return navigator.language;
|
||||
} else {
|
||||
return app.getLocale();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -58,8 +58,8 @@ describe("ClientTranslationsLoaderService", () => {
|
|||
it("it only loads the english translations file when locale is english", async () => {
|
||||
localService.locale = "en";
|
||||
await loader.load();
|
||||
expect(fsSpy.readFile).toHaveBeenCalledTimes(1);
|
||||
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "i18n-deprecated/resources.en.json"));
|
||||
expect(fsSpy.readFile).toHaveBeenCalledTimes(2);
|
||||
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "./resources/i18n/resources.en.json"));
|
||||
|
||||
expect(loader.translations.get("foo.banana")).toEqual("Banana");
|
||||
expect(loader.translations.get("foo.potato")).toEqual("Potato");
|
||||
|
@ -69,8 +69,8 @@ describe("ClientTranslationsLoaderService", () => {
|
|||
localService.locale = "fr";
|
||||
await loader.load();
|
||||
expect(fsSpy.readFile).toHaveBeenCalledTimes(2);
|
||||
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "i18n-deprecated/resources.en.json"));
|
||||
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "i18n-deprecated/resources.fr.json"));
|
||||
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "./resources/i18n/resources.en.json"));
|
||||
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "./resources/i18n/resources.fr.json"));
|
||||
|
||||
expect(loader.translations.get("foo.potato")).toEqual("Pomme de terre");
|
||||
expect(loader.translations.get("foo.banana")).toEqual("Banana", "Use english translation when not present");
|
||||
|
@ -90,7 +90,7 @@ describe("ClientTranslationsLoaderService", () => {
|
|||
localService.locale = "en";
|
||||
await loader.load();
|
||||
expect(devTranslationLoaderSpy.load).toHaveBeenCalledTimes(1);
|
||||
expect(fsSpy.readFile).not.toHaveBeenCalled();
|
||||
expect(fsSpy.readFile).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(loader.translations.get("foo.banana")).toEqual("Banana");
|
||||
expect(loader.translations.get("foo.potato")).toEqual("Potato");
|
||||
|
@ -102,7 +102,7 @@ describe("ClientTranslationsLoaderService", () => {
|
|||
await loader.load();
|
||||
expect(devTranslationLoaderSpy.load).toHaveBeenCalledTimes(1);
|
||||
expect(fsSpy.readFile).toHaveBeenCalledTimes(1);
|
||||
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "i18n-deprecated/resources.fr.json"));
|
||||
expect(fsSpy.readFile).toHaveBeenCalledWith(path.join(Constants.resourcesFolder, "./resources/i18n/resources.fr.json"));
|
||||
|
||||
expect(loader.translations.get("foo.potato")).toEqual("Pomme de terre");
|
||||
expect(loader.translations.get("foo.banana")).toEqual("Banana", "Use english translation when not present");
|
||||
|
|
|
@ -41,7 +41,7 @@ export class ClientTranslationsLoaderService extends TranslationsLoaderService {
|
|||
}
|
||||
|
||||
private async _loadProductionTranslations() {
|
||||
const englishTranslationFile = path.join(ClientConstants.resourcesFolder, "./i18n-deprecated/resources.en.json");
|
||||
const englishTranslationFile = path.join(ClientConstants.resourcesFolder, "./resources/i18n/resources.en.json");
|
||||
await this._loadProductionTranslationFile(englishTranslationFile);
|
||||
await this._loadLocaleTranslations();
|
||||
}
|
||||
|
@ -51,8 +51,7 @@ export class ClientTranslationsLoaderService extends TranslationsLoaderService {
|
|||
*/
|
||||
private async _loadLocaleTranslations() {
|
||||
const locale = this.localeService.locale;
|
||||
if (locale === Locale.English) { return; }
|
||||
const localeTranslationFile = path.join(ClientConstants.resourcesFolder, `./i18n-deprecated/resources.${locale}.json`);
|
||||
const localeTranslationFile = path.join(ClientConstants.resourcesFolder, `./resources/i18n/resources.${locale}.json`);
|
||||
if (await this.fs.exists(localeTranslationFile)) {
|
||||
await this._loadProductionTranslationFile(localeTranslationFile);
|
||||
} else {
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
# Building Localization on Local Machine
|
||||
|
||||
## Building the English File
|
||||
## Building the English File (Any Machine)
|
||||
|
||||
To build the latest English translation resjson file from the YAML files:
|
||||
To build the latest English translation file from the YAML files:
|
||||
|
||||
* Run `npm run build` to build translations for the entire repository
|
||||
* Run `npm run build-translations` in any package directory (desktop, packages/*) to build translations for a specific package
|
||||
* Run `npm run build-translations`
|
||||
|
||||
The output will be in `{packageName}/i18n/resources.resjson`
|
||||
* `web/dev-server/resources/i18n/resources.en.json` contains web English strings (web + all packages)
|
||||
* `desktop/resources/i18n/resources.en.json` contains desktop English strings (desktop + all packages)
|
||||
|
||||
## Building Translations Files for Other Languages (Windows-Only)
|
||||
|
||||
To build the localization translations for all languages besides English:
|
||||
|
||||
* Follow the steps above first
|
||||
* Follow the step above first (build the English file)
|
||||
* Install the latest, recommended version of nuget.exe from <https://www.nuget.org/downloads> at C:\Users\{userName}, for instance.
|
||||
* Navigate to the root of the repository
|
||||
* Run `npm run loc:restore` to install all dependencies
|
||||
* Run `npm run loc:build` to build the translations and move them to their correct directories
|
||||
* If needed, run `npm run clean` to clear out all previously built translation files
|
||||
* Run `npm run loc:build` to build the translations, move them to the package directories, and combine them altogether in one directory
|
||||
* Run `npm run build-translations` to build the full, compiled translations for the web and desktop packages
|
||||
|
||||
The output will be in `{packageName}/resources/i18n`
|
||||
|
||||
* `{packageName}/resources/i18n/resjson` contains RESJSON translations
|
||||
* `{packageName}/resources/i18n/json` contains JSON translations (RESJSON syntax and comments have been stripped out)
|
||||
* `web/dev-server/resources/i18n` contains web translations (web + all packages)
|
||||
* `desktop/resources/i18n` contains desktop translations (desktop + all packages)
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
"build:package": "lerna run workspace:build:package --stream",
|
||||
"build:prod": "lerna run workspace:build:prod --stream",
|
||||
"build:test": "lerna run workspace:build:test --stream",
|
||||
"clean": "lerna run --parallel workspace:clean --stream && npm run loc:clean",
|
||||
"build-translations": "lerna run workspace:build-translations --stream",
|
||||
"clean": "lerna run --parallel workspace:clean --stream && bux rmrf ./Localize/out",
|
||||
"gather-build-results": "bux gather-build-results",
|
||||
"launch": "npm run -s launch:web",
|
||||
"launch:desktop": "lerna run --parallel workspace:launch:desktop --stream",
|
||||
|
@ -64,7 +65,6 @@
|
|||
"lint:fix": "prettier -w . && lerna run --parallel workspace:lint:fix --stream && npm run -s lint:markdown",
|
||||
"lint:markdown": "markdownlint-cli2 \"**/*.md\" \"#**/node_modules/**/*\" \"#SECURITY.md\"",
|
||||
"loc:build": "powershell -ExecutionPolicy Bypass -File ./Localize/build.ps1 && powershell -ExecutionPolicy Bypass -File ./Localize/copy-translations.ps1",
|
||||
"loc:clean": "bux rmrf ./Localize/out",
|
||||
"loc:restore": "cd Localize && powershell -ExecutionPolicy Bypass -File restore.ps1",
|
||||
"start": "npm run -s start:web",
|
||||
"start:desktop": "lerna run --parallel workspace:start:desktop --stream",
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
{
|
||||
"lib.common.localizer.account": "Account",
|
||||
"lib.common.localizer.resourceGroup": "Resource Group",
|
||||
"lib.common.localizer.subscription": "Subscription"
|
||||
"lib.common.form.validationError": "Value must be a boolean"
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
"build": "npm run compile && npm run build-translations",
|
||||
"build:clean": "run-s clean compile",
|
||||
"build:test": "run-s build test",
|
||||
"build-translations": "bux build-translations --src src/ui-common --dest i18n --packageName lib.common",
|
||||
"build-translations": "bux build-translations --src src/ui-common --dest i18n --outputPath resources/i18n/json --packageName lib.common",
|
||||
"compile": "run-p compile:*",
|
||||
"compile:esm": "tsc -b ./config/tsconfig.build.json",
|
||||
"compile:cjs": "tsc -b ./config/tsconfig.cjs.json",
|
||||
|
@ -41,6 +41,7 @@
|
|||
"workspace:build:package": "npm run build:clean",
|
||||
"workspace:build:prod": "npm run build:clean",
|
||||
"workspace:build:test": "npm run build:test",
|
||||
"workspace:build-translations": "npm run build-translations",
|
||||
"workspace:clean": "npm run clean",
|
||||
"workspace:launch:desktop": "npm run watch",
|
||||
"workspace:launch:web": "npm run watch",
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import { HttpLocalizer } from "../localization/http-localizer";
|
||||
|
||||
describe("HttpLocalizer", () => {
|
||||
let httpLocalizer: HttpLocalizer;
|
||||
let fetchMock: jest.Mock;
|
||||
|
||||
const testTranslations = { hello: "world" };
|
||||
const frenchTranslations = { bonjour: "monde" };
|
||||
|
||||
beforeEach(() => {
|
||||
httpLocalizer = new HttpLocalizer();
|
||||
fetchMock = jest.fn();
|
||||
global.fetch = fetchMock;
|
||||
});
|
||||
|
||||
test("Load the correct translation file based on the locale", async () => {
|
||||
fetchMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(testTranslations),
|
||||
})
|
||||
);
|
||||
|
||||
jest.spyOn(httpLocalizer, "getLocale").mockReturnValue("en-US");
|
||||
await httpLocalizer.loadTranslations();
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/resources/i18n/resources.en.json"
|
||||
);
|
||||
expect(httpLocalizer.translate("hello")).toEqual("world");
|
||||
});
|
||||
|
||||
test("Load the correct translation file for French locale", async () => {
|
||||
fetchMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(frenchTranslations),
|
||||
})
|
||||
);
|
||||
|
||||
// Simulate a French locale
|
||||
jest.spyOn(httpLocalizer, "getLocale").mockReturnValue("fr-FR");
|
||||
await httpLocalizer.loadTranslations();
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/resources/i18n/resources.fr.json"
|
||||
);
|
||||
expect(httpLocalizer.translate("bonjour")).toEqual("monde");
|
||||
});
|
||||
|
||||
test("Default to English if locale not found", async () => {
|
||||
fetchMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(testTranslations),
|
||||
})
|
||||
);
|
||||
|
||||
// Simulate an invalid locale
|
||||
jest.spyOn(httpLocalizer, "getLocale").mockReturnValue("abc");
|
||||
await httpLocalizer.loadTranslations();
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/resources/i18n/resources.en.json"
|
||||
);
|
||||
});
|
||||
|
||||
test("Throw error if translations have not been loaded", () => {
|
||||
jest.spyOn(httpLocalizer, "getLocale").mockReturnValue("en-US");
|
||||
expect(() => httpLocalizer.translate("hello")).toThrowError(
|
||||
"Translation strings are not loaded hello"
|
||||
);
|
||||
});
|
||||
|
||||
test("Return original message if no translation found", async () => {
|
||||
fetchMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(testTranslations),
|
||||
})
|
||||
);
|
||||
|
||||
jest.spyOn(httpLocalizer, "getLocale").mockReturnValue("en-US");
|
||||
await httpLocalizer.loadTranslations();
|
||||
expect(httpLocalizer.translate("notFound")).toEqual("notFound");
|
||||
});
|
||||
});
|
|
@ -7,7 +7,9 @@ describe("Localization utilities", () => {
|
|||
});
|
||||
|
||||
test("Can return simple english strings", () => {
|
||||
expect(translate("subscription")).toEqual("Subscription");
|
||||
expect(translate("lib.common.form.validationError")).toEqual(
|
||||
"Value must be a boolean"
|
||||
);
|
||||
});
|
||||
|
||||
test("Throw error if string is unknown", () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FakeClock } from "../../datetime/fake-clock";
|
||||
import { MockHttpClient } from "../../http";
|
||||
import { StandardLocalizer } from "../../localization/standard-localizer";
|
||||
import { FakeLocalizer } from "../../localization/fake-localizer";
|
||||
import { createMockLogger } from "../../logging";
|
||||
import { AbstractEnvironment } from "../abstract-environment";
|
||||
import {
|
||||
|
@ -150,7 +150,7 @@ describe("Environment tests", () => {
|
|||
super(config, {
|
||||
clock: () => new FakeClock(),
|
||||
loggerFactory: () => createMockLogger,
|
||||
localizer: () => new StandardLocalizer(),
|
||||
localizer: () => new FakeLocalizer(),
|
||||
httpClient: () => new MockHttpClient(),
|
||||
animal: () => new Dog(),
|
||||
});
|
||||
|
@ -175,7 +175,7 @@ describe("Environment tests", () => {
|
|||
super(config, {
|
||||
clock: () => new FakeClock(),
|
||||
loggerFactory: () => createMockLogger,
|
||||
localizer: () => new StandardLocalizer(),
|
||||
localizer: () => new FakeLocalizer(),
|
||||
httpClient: () => new MockHttpClient(),
|
||||
animal: () => new Cat(),
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { DependencyFactories } from ".";
|
|||
import { setLocalTimeZoneOffset } from "../datetime";
|
||||
import { FakeClock } from "../datetime/fake-clock";
|
||||
import { MockHttpClient } from "../http/mock-http-client";
|
||||
import { StandardLocalizer } from "../localization/standard-localizer";
|
||||
import { FakeLocalizer } from "../localization/fake-localizer";
|
||||
import { createMockLogger } from "../logging";
|
||||
import { AbstractEnvironment } from "./abstract-environment";
|
||||
import {
|
||||
|
@ -18,7 +18,7 @@ export const mockEnvironmentConfig: EnvironmentConfig = {
|
|||
export const mockDependencyFactories: DependencyFactories = {
|
||||
clock: () => new FakeClock(),
|
||||
loggerFactory: () => createMockLogger,
|
||||
localizer: () => new StandardLocalizer(),
|
||||
localizer: () => new FakeLocalizer(),
|
||||
httpClient: () => new MockHttpClient(),
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
form:
|
||||
validationError: Value must be a boolean
|
|
@ -1,4 +0,0 @@
|
|||
localizer:
|
||||
account: Account
|
||||
resourceGroup: Resource Group
|
||||
subscription: Subscription
|
|
@ -0,0 +1,28 @@
|
|||
import { Localizer } from "./localizer";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const STRINGS = (globalThis as unknown as any).__TEST_RESOURCE_STRINGS;
|
||||
|
||||
export class FakeLocalizer implements Localizer {
|
||||
private locale: string;
|
||||
|
||||
constructor() {
|
||||
this.locale = "en";
|
||||
}
|
||||
|
||||
translate(message: string): string {
|
||||
const value = STRINGS[message];
|
||||
if (value == null) {
|
||||
throw new Error("Unable to translate string " + message);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
getLocale(): string {
|
||||
return this.locale;
|
||||
}
|
||||
|
||||
setLocale(locale: string): void {
|
||||
this.locale = locale;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import { Localizer } from "./localizer";
|
||||
|
||||
interface Translations {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export class HttpLocalizer implements Localizer {
|
||||
private translations?: Translations;
|
||||
|
||||
async loadTranslations(): Promise<void> {
|
||||
const languageMap: Record<string, string> = {
|
||||
cs: "cs",
|
||||
de: "de",
|
||||
en: "en",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
hu: "hu",
|
||||
id: "id",
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
ko: "ko",
|
||||
nl: "nl",
|
||||
pl: "pl",
|
||||
pt: "pt-PT",
|
||||
"pt-PT": "pt-PT",
|
||||
"pt-BR": "pt-BR",
|
||||
ru: "ru",
|
||||
sv: "sv",
|
||||
tr: "tr",
|
||||
zh: "zh-Hans",
|
||||
"zh-CN": "zh-Hans",
|
||||
"zh-TW": "zh-Hant",
|
||||
};
|
||||
|
||||
let languageToLoad = languageMap[this.getLocale()];
|
||||
|
||||
if (!languageToLoad) {
|
||||
// If not found, split and check for two-digit codes
|
||||
const language = this.getLocale().split("-")[0];
|
||||
languageToLoad = languageMap[language]
|
||||
? languageMap[language]
|
||||
: "en";
|
||||
}
|
||||
|
||||
try {
|
||||
this.translations = await this.fetchTranslations(languageToLoad);
|
||||
} catch (error) {
|
||||
// Fall back to English if translations are not available for the selected locale
|
||||
if (languageToLoad !== "en") {
|
||||
console.error(
|
||||
`Failed to load translations for '${languageToLoad}', falling back to English: ${
|
||||
(error as Error).message
|
||||
}`
|
||||
);
|
||||
languageToLoad = "en";
|
||||
this.translations = await this.fetchTranslations(
|
||||
languageToLoad
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to load translations for '${languageToLoad}': ${
|
||||
(error as Error).message
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchTranslations(lang: string): Promise<Translations> {
|
||||
const response = await fetch(`/resources/i18n/resources.${lang}.json`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to load translations for '${lang}': ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
translate(message: string): string {
|
||||
if (!this.translations) {
|
||||
throw new Error("Translation strings are not loaded " + message);
|
||||
}
|
||||
const translation = this.translations[message];
|
||||
if (translation != null) {
|
||||
return translation;
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
getLocale(): string {
|
||||
return navigator.language;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
export * from "./localization-util";
|
||||
export * from "./localizer";
|
||||
export * from "./standard-localizer";
|
||||
export * from "./http-localizer";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export interface Localizer {
|
||||
translate(message: string): string;
|
||||
getLocale(): string;
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import { Localizer } from "./localizer";
|
||||
|
||||
export class StandardLocalizer implements Localizer {
|
||||
translate(message: string): string {
|
||||
switch (message) {
|
||||
case "subscription":
|
||||
return "Subscription";
|
||||
case "resourceGroup":
|
||||
return "Resource Group";
|
||||
case "accountName":
|
||||
return "Account Name";
|
||||
case "form.buttons.apply":
|
||||
return "Apply";
|
||||
case "form.buttons.discardChanges":
|
||||
return "Discard changes";
|
||||
}
|
||||
|
||||
throw new Error("Unable to translate string " + message);
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
"build:clean": "run-s clean compile",
|
||||
"build:test": "run-s build test",
|
||||
"bux": "bux",
|
||||
"build-translations": "bux build-translations --src src/ui-playground --dest i18n --packageName lib.playground",
|
||||
"build-translations": "bux build-translations --src src/ui-playground --dest i18n --outputPath resources/i18n/json --packageName lib.playground",
|
||||
"compile": "run-p compile:*",
|
||||
"compile:esm": "tsc -b ./config/tsconfig.build.json",
|
||||
"compile:cjs": "tsc -b ./config/tsconfig.cjs.json",
|
||||
|
@ -42,6 +42,7 @@
|
|||
"workspace:build:package": "npm run build:clean",
|
||||
"workspace:build:prod": "npm run build:clean",
|
||||
"workspace:build:test": "npm run build:test",
|
||||
"workspace:build-translations": "npm run build-translations",
|
||||
"workspace:clean": "npm run clean",
|
||||
"workspace:launch:desktop": "npm run watch",
|
||||
"workspace:launch:web": "npm run watch",
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"lib.react.create-account-action.location": "Location",
|
||||
"lib.react.create-account-action.storageAccount": "Storage Account"
|
||||
"lib.react.account.parameter.name.label": "Account name",
|
||||
"lib.react.arm.location": "Location",
|
||||
"lib.react.arm.resourceGroup": "Resource group",
|
||||
"lib.react.arm.storageAccount": "Storage account",
|
||||
"lib.react.arm.subscription": "Subscription",
|
||||
"lib.react.form.buttons.apply": "Apply",
|
||||
"lib.react.form.buttons.discardChanges": "Discard changes"
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
"build:clean": "run-s clean compile",
|
||||
"build:test": "run-s build test",
|
||||
"bux": "bux",
|
||||
"build-translations": "bux build-translations --src src/ui-react --dest i18n --packageName lib.react",
|
||||
"build-translations": "bux build-translations --src src/ui-react --dest i18n --outputPath resources/i18n/json --packageName lib.react",
|
||||
"compile": "run-p compile:*",
|
||||
"compile:esm": "tsc -b ./config/tsconfig.build.json",
|
||||
"compile:cjs": "tsc -b ./config/tsconfig.cjs.json",
|
||||
|
@ -44,6 +44,7 @@
|
|||
"workspace:build:package": "npm run build:clean",
|
||||
"workspace:build:prod": "npm run build:clean",
|
||||
"workspace:build:test": "npm run build:test",
|
||||
"workspace:build-translations": "npm run build-translations",
|
||||
"workspace:clean": "npm run clean",
|
||||
"workspace:launch:desktop": "npm run watch",
|
||||
"workspace:launch:web": "npm run watch",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
account:
|
||||
parameter:
|
||||
name:
|
||||
label: Account name
|
|
@ -0,0 +1,5 @@
|
|||
arm:
|
||||
location: Location
|
||||
resourceGroup: Resource group
|
||||
storageAccount: Storage account
|
||||
subscription: Subscription
|
|
@ -52,18 +52,18 @@ export class CreateAccountAction extends AbstractAction<CreateAccountFormValues>
|
|||
values: initialValues,
|
||||
});
|
||||
form.param("subscriptionId", SubscriptionParameter, {
|
||||
label: translate("subscription"),
|
||||
label: translate("lib.react.arm.subscription"),
|
||||
required: true,
|
||||
});
|
||||
form.param("resourceGroupId", ResourceGroupParameter, {
|
||||
dependencies: {
|
||||
subscriptionId: "subscriptionId",
|
||||
},
|
||||
label: translate("resourceGroup"),
|
||||
label: translate("lib.react.arm.resourceGroup"),
|
||||
required: true,
|
||||
});
|
||||
form.param("accountName", StringParameter, {
|
||||
label: translate("accountName"),
|
||||
label: translate("lib.react.account.parameter.name.label"),
|
||||
required: true,
|
||||
description:
|
||||
"This is how you identify your Batch account. It must be unique.",
|
||||
|
@ -96,14 +96,14 @@ export class CreateAccountAction extends AbstractAction<CreateAccountFormValues>
|
|||
dependencies: {
|
||||
subscriptionId: "subscriptionId",
|
||||
},
|
||||
label: "Location",
|
||||
label: translate("lib.react.arm.location"),
|
||||
required: true,
|
||||
});
|
||||
form.param("storageAccountId", StorageAccountParameter, {
|
||||
dependencies: {
|
||||
subscriptionId: "subscriptionId",
|
||||
},
|
||||
label: "Storage account",
|
||||
label: translate("lib.react.arm.storageAccount"),
|
||||
description:
|
||||
"Optional. For best performance we recommend a storage account (general purpose v2) located in the same region as the associated Batch account.",
|
||||
});
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
create-account-action:
|
||||
location: Location
|
||||
storageAccount: Storage Account
|
|
@ -148,7 +148,9 @@ export const ActionForm = <V extends FormValues>(
|
|||
if (hideSubmitButton !== true) {
|
||||
// Default submit button
|
||||
allButtons.push({
|
||||
label: props.submitButtonLabel ?? translate("form.buttons.apply"),
|
||||
label:
|
||||
props.submitButtonLabel ??
|
||||
translate("lib.react.form.buttons.apply"),
|
||||
primary: true,
|
||||
submitForm: true,
|
||||
disabled: submitting === true,
|
||||
|
@ -195,7 +197,8 @@ export const ActionForm = <V extends FormValues>(
|
|||
// Default reset (discard) button
|
||||
allButtons.push({
|
||||
label:
|
||||
submitButtonLabel ?? translate("form.buttons.discardChanges"),
|
||||
submitButtonLabel ??
|
||||
translate("lib.react.form.buttons.discardChanges"),
|
||||
disabled: submitting === true,
|
||||
onClick: () => {
|
||||
action.form.reset();
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
form:
|
||||
buttons:
|
||||
apply: Apply
|
||||
discardChanges: Discard changes
|
|
@ -17,7 +17,7 @@
|
|||
"build": "npm run compile && npm run build-translations",
|
||||
"build:clean": "run-s clean compile",
|
||||
"build:test": "run-s build test",
|
||||
"build-translations": "bux build-translations --src src/ui-service --dest i18n --packageName lib.service",
|
||||
"build-translations": "bux build-translations --src src/ui-service --dest i18n --outputPath resources/i18n/json --packageName lib.service",
|
||||
"compile": "run-p compile:*",
|
||||
"compile:esm": "tsc -b ./config/tsconfig.build.json",
|
||||
"compile:cjs": "tsc -b ./config/tsconfig.cjs.json",
|
||||
|
@ -41,6 +41,7 @@
|
|||
"workspace:build:package": "npm run build:clean",
|
||||
"workspace:build:prod": "npm run build:clean",
|
||||
"workspace:build:test": "npm run build:test",
|
||||
"workspace:build-translations": "npm run build-translations",
|
||||
"workspace:clean": "npm run clean",
|
||||
"workspace:launch:desktop": "npm run watch",
|
||||
"workspace:launch:web": "npm run watch",
|
||||
|
|
|
@ -54,19 +54,22 @@ describe("CLI", () => {
|
|||
if (err) throw err;
|
||||
});
|
||||
|
||||
//Test that localization build script works correctly WITHOUT packageName parameter
|
||||
//Test that localization build script works correctly
|
||||
test("Localization", async () => {
|
||||
await util.buildTranslations(
|
||||
"./__tests__/loc-source",
|
||||
"./build/test-results/loc-results-1"
|
||||
"./build/test-results/loc-results-1/resjson",
|
||||
"./build/test-results/loc-results-1/json",
|
||||
"lib.common"
|
||||
);
|
||||
});
|
||||
|
||||
//Test that localization build script works correctly WITH packageName parameter
|
||||
//Test that localization build script works correctly
|
||||
test("Localization with package name", async () => {
|
||||
await util.buildTranslations(
|
||||
"./__tests__/loc-source",
|
||||
"./build/test-results/loc-results-2",
|
||||
"./build/test-results/loc-results-2/resjson",
|
||||
"./build/test-results/loc-results-2/json",
|
||||
"lib.common"
|
||||
);
|
||||
});
|
||||
|
@ -74,7 +77,7 @@ describe("CLI", () => {
|
|||
//Check that the RESJSON file with the packageName parameter contains the package name in its contents
|
||||
test("Check if RESJSON file with packageName parameter contains the package name", () => {
|
||||
const data = fs.readFileSync(
|
||||
"./build/test-results/loc-results-2/resources.resjson"
|
||||
"./build/test-results/loc-results-2/resjson/resources.resjson"
|
||||
);
|
||||
expect(data.toString()).toContain("lib.common");
|
||||
});
|
||||
|
@ -85,12 +88,15 @@ describe("CLI", () => {
|
|||
await expect(
|
||||
createEnglishTranslations(
|
||||
"./__tests__/loc-source-2",
|
||||
"./build/test-results/loc-results-3"
|
||||
"./build/test-results/loc-results-3/resjson",
|
||||
"./build/test-results/loc-results-3/json"
|
||||
)
|
||||
).rejects.toThrow(Error);
|
||||
|
||||
// expect destination directory to be empty due to error
|
||||
const files = fs.readdirSync("./build/test-results/loc-results-3");
|
||||
expect(files.length).toBe(0);
|
||||
const directoryContents = fs.readdirSync(
|
||||
"./build/test-results/loc-results-3"
|
||||
);
|
||||
expect(directoryContents.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ yargs
|
|||
command: "build-translations",
|
||||
aliases: ["translate"],
|
||||
describe:
|
||||
"Localize YAML files in src and convert them to RESJSON in dest",
|
||||
"Localize YAML files, convert them to RESJSON, and generate English JSON file",
|
||||
builder: (yargs: yargs.Argv) =>
|
||||
yargs
|
||||
.option("src", {
|
||||
|
@ -44,7 +44,13 @@ yargs
|
|||
})
|
||||
.option("dest", {
|
||||
describe:
|
||||
"The destination directory for RESJSON output files",
|
||||
"The destination directory for the English RESJSON file used by the localization team",
|
||||
default: "",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("outputPath", {
|
||||
describe:
|
||||
"The directory that contains the generated English JSON translation file",
|
||||
default: "",
|
||||
demandOption: true,
|
||||
})
|
||||
|
@ -55,7 +61,12 @@ yargs
|
|||
demandOption: false,
|
||||
}),
|
||||
handler: (argv) =>
|
||||
buildTranslations(argv.src, argv.dest, argv.packageName),
|
||||
buildTranslations(
|
||||
argv.src,
|
||||
argv.dest,
|
||||
argv.outputPath,
|
||||
argv.packageName
|
||||
),
|
||||
})
|
||||
.command({
|
||||
command: "configure",
|
||||
|
|
|
@ -13,9 +13,130 @@ type NestedStringMap<V> = StringMap<V> | StringMap<StringMap<V>>;
|
|||
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
|
||||
const readFileAsync = promisify(fs.readFile);
|
||||
const writeFileAsync = promisify(fs.writeFile);
|
||||
|
||||
export async function mergeAllTranslations(outputPath: string) {
|
||||
const currentDirectory = process.cwd();
|
||||
const rootDir = path.resolve(currentDirectory, "..");
|
||||
|
||||
// Ensure the output directory exists
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
fs.mkdirSync(outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Define resource directories (absolute paths)
|
||||
const resourceDirs = [
|
||||
path.join(rootDir, "packages/common/resources/i18n/json"),
|
||||
path.join(rootDir, "packages/playground/resources/i18n/json"),
|
||||
path.join(rootDir, "packages/react/resources/i18n/json"),
|
||||
path.join(rootDir, "packages/service/resources/i18n/json"),
|
||||
];
|
||||
|
||||
// Build the English file for each package before building web/desktop translations (it is a prerequisite)
|
||||
|
||||
const buildPackageEnglishPromises = [];
|
||||
|
||||
buildPackageEnglishPromises.push(
|
||||
createEnglishTranslations(
|
||||
path.join(rootDir, "packages/react/src/ui-react"),
|
||||
path.join(rootDir, "packages/react/i18n"),
|
||||
path.join(rootDir, "packages/react/resources/i18n/json"),
|
||||
"lib.react"
|
||||
)
|
||||
);
|
||||
buildPackageEnglishPromises.push(
|
||||
createEnglishTranslations(
|
||||
path.join(rootDir, "packages/playground/src/ui-playground"),
|
||||
path.join(rootDir, "packages/playground/i18n"),
|
||||
path.join(rootDir, "packages/playground/resources/i18n/json"),
|
||||
"lib.playground"
|
||||
)
|
||||
);
|
||||
buildPackageEnglishPromises.push(
|
||||
createEnglishTranslations(
|
||||
path.join(rootDir, "packages/common/src/ui-common"),
|
||||
path.join(rootDir, "packages/common/i18n"),
|
||||
path.join(rootDir, "packages/common/resources/i18n/json"),
|
||||
"lib.common"
|
||||
)
|
||||
);
|
||||
buildPackageEnglishPromises.push(
|
||||
createEnglishTranslations(
|
||||
path.join(rootDir, "packages/service/src/ui-service"),
|
||||
path.join(rootDir, "packages/service/i18n"),
|
||||
path.join(rootDir, "packages/service/resources/i18n/json"),
|
||||
"lib.service"
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all(buildPackageEnglishPromises);
|
||||
|
||||
// Initialize an empty object to store the merged translations
|
||||
const mergedTranslations: Record<string, Record<string, string>> = {};
|
||||
|
||||
// Iterate through each resource directory
|
||||
for (const dir of resourceDirs) {
|
||||
// Iterate through each JSON file in the directory
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
if (file.startsWith("resources.") && file.endsWith(".json")) {
|
||||
const langID = file.split(".")[1];
|
||||
|
||||
// If the language ID is not in the object, add it
|
||||
if (!mergedTranslations[langID]) {
|
||||
mergedTranslations[langID] = {};
|
||||
}
|
||||
|
||||
// Read the JSON content and parse it
|
||||
const content = JSON.parse(
|
||||
await readFileAsync(path.join(dir, file), "utf-8")
|
||||
);
|
||||
|
||||
// Merge the content into the object
|
||||
Object.assign(mergedTranslations[langID], content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the merged translations to the output directory
|
||||
for (const langID of Object.keys(mergedTranslations)) {
|
||||
const outputFile = path.join(outputPath, `resources.${langID}.json`);
|
||||
|
||||
// Read existing translations in the output file if it exists
|
||||
let existingTranslations = {};
|
||||
if (fs.existsSync(outputFile)) {
|
||||
existingTranslations = JSON.parse(
|
||||
await readFileAsync(outputFile, "utf-8")
|
||||
);
|
||||
}
|
||||
|
||||
// Merge existing translations with new translations
|
||||
const combinedTranslations = {
|
||||
...existingTranslations,
|
||||
...mergedTranslations[langID],
|
||||
};
|
||||
|
||||
// Sort keys alphabetically
|
||||
const sortedTranslations = Object.fromEntries(
|
||||
Object.entries(combinedTranslations).sort()
|
||||
);
|
||||
|
||||
// Write the sorted translations to the output file
|
||||
await writeFileAsync(
|
||||
outputFile,
|
||||
JSON.stringify(sortedTranslations, null, 2),
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Merged translations have been saved in ${outputPath}`);
|
||||
}
|
||||
|
||||
// Function to generate English file for a package from its YAML files
|
||||
export async function createEnglishTranslations(
|
||||
sourcePath: string,
|
||||
destPath: string,
|
||||
outputPath: string,
|
||||
packageName?: string
|
||||
) {
|
||||
const translations = await loadDevTranslations(sourcePath, packageName);
|
||||
|
@ -50,14 +171,7 @@ export async function createEnglishTranslations(
|
|||
}
|
||||
|
||||
const cleanedJsonContent = JSON.stringify(cleanContent, null, 2);
|
||||
|
||||
const resourcesJsonPath = path.join(
|
||||
path.dirname(path.dirname(sourcePath)),
|
||||
"resources",
|
||||
"i18n",
|
||||
"json",
|
||||
"resources.en.json"
|
||||
);
|
||||
const resourcesJsonPath = path.join(outputPath, "resources.en.json");
|
||||
|
||||
// Check if the directory exists and create it if it doesn't
|
||||
if (!fs.existsSync(path.dirname(resourcesJsonPath))) {
|
||||
|
@ -78,13 +192,11 @@ export async function loadDevTranslations(
|
|||
packageName?: string
|
||||
): Promise<{ [key: string]: string }> {
|
||||
const loader = new DevTranslationsLoader();
|
||||
console.log("Loading dev translations...");
|
||||
let hasDuplicate = false;
|
||||
const translations = await loader.load(sourcePath, (key, file) => {
|
||||
console.warn(`${key} is being duplicated. "${file}"`);
|
||||
hasDuplicate = true;
|
||||
});
|
||||
console.log(`Loaded dev translations`);
|
||||
if (hasDuplicate) {
|
||||
throw new Error();
|
||||
}
|
||||
|
|
|
@ -9,7 +9,10 @@ import inquirer from "inquirer";
|
|||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as shell from "shelljs";
|
||||
import { createEnglishTranslations } from "./translation-functions";
|
||||
import {
|
||||
createEnglishTranslations,
|
||||
mergeAllTranslations,
|
||||
} from "./translation-functions";
|
||||
|
||||
export const defaultBatchExplorerHome = path.resolve(__dirname, "../../");
|
||||
export const configFile = path.resolve(os.homedir(), ".config/batch/bux.json");
|
||||
|
@ -104,6 +107,7 @@ export function copyFiles(sourcePath: string, destPath: string) {
|
|||
export async function buildTranslations(
|
||||
sourcePath: string,
|
||||
destPathRESJSON: string,
|
||||
outputPath: string,
|
||||
packageName?: string
|
||||
) {
|
||||
if (!sourcePath) {
|
||||
|
@ -126,7 +130,22 @@ export async function buildTranslations(
|
|||
});
|
||||
}
|
||||
|
||||
await createEnglishTranslations(sourcePath, destPathRESJSON, packageName);
|
||||
if (!outputPath) {
|
||||
error("Failed to build translations: No output path specified");
|
||||
return;
|
||||
}
|
||||
|
||||
await createEnglishTranslations(
|
||||
sourcePath,
|
||||
destPathRESJSON,
|
||||
outputPath,
|
||||
packageName
|
||||
);
|
||||
|
||||
// If no packageName, merge all translations (for web/desktop). packageName indicates package-specific operation.
|
||||
if (!packageName) {
|
||||
await mergeAllTranslations(outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
export function mkdirp(targetPath: string) {
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { readFileSync } = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const combinedResourceStrings = getCombinedResourceStrings();
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Creates a Jest configuration object with optional overrides.
|
||||
|
@ -50,6 +55,7 @@ module.exports = {
|
|||
],
|
||||
],
|
||||
globals: {
|
||||
__TEST_RESOURCE_STRINGS: combinedResourceStrings,
|
||||
"ts-jest": {
|
||||
diagnostics: {
|
||||
// Squelch a warning with outputting ES6 modules (in tsconfig.json)
|
||||
|
@ -95,3 +101,31 @@ module.exports = {
|
|||
return Object.assign({}, baseConfig, overrides);
|
||||
},
|
||||
};
|
||||
|
||||
function getCombinedResourceStrings() {
|
||||
const resourceStrings = [
|
||||
require(path.join(
|
||||
__dirname,
|
||||
"../../packages/common/resources/i18n/json/resources.en.json"
|
||||
)),
|
||||
require(path.join(
|
||||
__dirname,
|
||||
"../../packages/service/resources/i18n/json/resources.en.json"
|
||||
)),
|
||||
require(path.join(
|
||||
__dirname,
|
||||
"../../packages/react/resources/i18n/json/resources.en.json"
|
||||
)),
|
||||
require(path.join(
|
||||
__dirname,
|
||||
"../../packages/playground/resources/i18n/json/resources.en.json"
|
||||
)),
|
||||
];
|
||||
|
||||
const allResourceStrings = {};
|
||||
resourceStrings.forEach((strings) => {
|
||||
Object.assign(allResourceStrings, strings);
|
||||
});
|
||||
|
||||
return allResourceStrings;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"application.buttons.editor": "Editor",
|
||||
"application.buttons.home": "Home",
|
||||
"application.buttons.playground": "Playground"
|
||||
}
|
|
@ -19,15 +19,17 @@
|
|||
"build:clean": "run-s clean compile bundle:dev",
|
||||
"build:prod": "run-s clean compile bundle:prod",
|
||||
"build:test": "run-s build test",
|
||||
"build-translations": "bux build-translations --src src/explorer-web --dest i18n --outputPath ./dev-server/resources/i18n",
|
||||
"bundle": "npm run bundle:dev",
|
||||
"bundle:analyze": "webpack --env.analyze",
|
||||
"bundle:dev": "webpack --env.dev",
|
||||
"bundle:prod": "webpack",
|
||||
"bux": "bux",
|
||||
"compile": "tsc -b ./config/tsconfig.build.json",
|
||||
"compile": "npm run build-translations && tsc -b ./config/tsconfig.build.json",
|
||||
"clean": "run-p clean:*",
|
||||
"clean:build": "bux rmrf ./build",
|
||||
"clean:esm": "bux rmrf ./lib",
|
||||
"clean:resources": "bux rmrf ./dev-server/resources",
|
||||
"clean:umd": "bux rmrf ./lib-umd",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --collect-coverage",
|
||||
|
@ -37,7 +39,7 @@
|
|||
"lint": "eslint .",
|
||||
"server": "webpack-dev-server --inline --hot",
|
||||
"start": "npm run start:dev",
|
||||
"start:dev": "npm run clean && npm run server -- --env.dev",
|
||||
"start:dev": "npm run clean && npm run build-translations && npm run server -- --env.dev",
|
||||
"start:prod": "npm run clean && npm run server",
|
||||
"watch": "npm run compile -- --env.watch",
|
||||
"workspace:build": "npm run build",
|
||||
|
@ -46,6 +48,7 @@
|
|||
"workspace:build:package": "npm run build:prod",
|
||||
"workspace:build:prod": "npm run build:prod",
|
||||
"workspace:build:test": "npm run build:test",
|
||||
"workspace:build-translations": "npm run build-translations",
|
||||
"workspace:clean": "npm run clean",
|
||||
"workspace:launch:web": "npm run start -- --env.launch",
|
||||
"workspace:lint": "npm run lint",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
application:
|
||||
buttons:
|
||||
home: Home
|
||||
editor: Editor
|
||||
playground: Playground
|
|
@ -15,6 +15,7 @@ import { Header } from "./layout/header";
|
|||
import { Main } from "./layout/main";
|
||||
import { Stack, IStackTokens } from "@fluentui/react/";
|
||||
import { PrimaryButton } from "@fluentui/react/lib/Button";
|
||||
import { translate } from "@batch/ui-common";
|
||||
|
||||
//DefaultButton
|
||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
|
@ -51,13 +52,16 @@ export const Application: React.FC = () => {
|
|||
<BrowserRouter>
|
||||
<Header>
|
||||
<Stack horizontal tokens={stackTokens}>
|
||||
<PrimaryButton text="Home" href="/"></PrimaryButton>
|
||||
<PrimaryButton
|
||||
text="Editor"
|
||||
text={translate("application.buttons.home")}
|
||||
href="/"
|
||||
></PrimaryButton>
|
||||
<PrimaryButton
|
||||
text={translate("application.buttons.editor")}
|
||||
href="/editor"
|
||||
></PrimaryButton>
|
||||
<PrimaryButton
|
||||
text="Playground"
|
||||
text={translate("application.buttons.playground")}
|
||||
href="/playground"
|
||||
></PrimaryButton>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
DefaultFormLayoutProvider,
|
||||
DefaultFormControlResolver,
|
||||
} from "@batch/ui-react/lib/components/form";
|
||||
import { StandardLocalizer } from "@batch/ui-common/lib/localization";
|
||||
import { HttpLocalizer } from "@batch/ui-common/lib/localization";
|
||||
import {
|
||||
FakeStorageAccountService,
|
||||
FakeSubscriptionService,
|
||||
|
@ -27,7 +27,9 @@ declare const ENV: {
|
|||
MODE: EnvironmentMode;
|
||||
};
|
||||
|
||||
export function init(rootEl: HTMLElement): void {
|
||||
export async function init(rootEl: HTMLElement): Promise<void> {
|
||||
const localizer = new HttpLocalizer();
|
||||
await localizer.loadTranslations();
|
||||
initEnvironment(
|
||||
new DefaultBrowserEnvironment(
|
||||
{
|
||||
|
@ -36,7 +38,7 @@ export function init(rootEl: HTMLElement): void {
|
|||
{
|
||||
[DependencyName.Clock]: () => new StandardClock(),
|
||||
[DependencyName.LoggerFactory]: () => createConsoleLogger,
|
||||
[DependencyName.Localizer]: () => new StandardLocalizer(),
|
||||
[DependencyName.Localizer]: () => localizer,
|
||||
[DependencyName.HttpClient]: () => new MockHttpClient(),
|
||||
[BrowserDependencyName.LocationService]: () =>
|
||||
new FakeLocationService(),
|
||||
|
|
Загрузка…
Ссылка в новой задаче