diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a03966bb00..8948f51df5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: env: CI: true - name: Build resources - run: ./node_modules/.bin/gulp gen-icons-json build-translations gather-gallery-demos + run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-demos - name: Run eslint run: yarn run lint:eslint - name: Run tsc @@ -53,6 +53,8 @@ jobs: run: yarn install env: CI: true + - name: Build resources + run: ./node_modules/.bin/gulp build-translations build-locale-data - name: Run Tests run: yarn run test build: diff --git a/.gitignore b/.gitignore index 31a7d3e588..c175017a6f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ # build build -build-translations/* hass_frontend/* dist diff --git a/.prettierignore b/.prettierignore index ee4c3bd66b..68fe46e32c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,4 @@ build -build-translations/* translations/* node_modules/* hass_frontend/* diff --git a/build-scripts/gulp/app.js b/build-scripts/gulp/app.js index 0cda8a6f30..2959ad6a55 100644 --- a/build-scripts/gulp/app.js +++ b/build-scripts/gulp/app.js @@ -5,6 +5,7 @@ const env = require("../env"); require("./clean.js"); require("./translations.js"); +require("./locale-data.js"); require("./gen-icons-json.js"); require("./gather-static.js"); require("./compress.js"); @@ -26,7 +27,8 @@ gulp.task( "gen-icons-json", "gen-pages-dev", "gen-index-app-dev", - "build-translations" + "build-translations", + "build-locale-data" ), "copy-static-app", env.useWDS() @@ -44,7 +46,7 @@ gulp.task( process.env.NODE_ENV = "production"; }, "clean", - gulp.parallel("gen-icons-json", "build-translations"), + gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), "copy-static-app", env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", // Don't compress running tests diff --git a/build-scripts/gulp/cast.js b/build-scripts/gulp/cast.js index cf5eea7714..3623cc9925 100644 --- a/build-scripts/gulp/cast.js +++ b/build-scripts/gulp/cast.js @@ -18,7 +18,7 @@ gulp.task( }, "clean-cast", "translations-enable-merge-backend", - gulp.parallel("gen-icons-json", "build-translations"), + gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), "copy-static-cast", "gen-index-cast-dev", env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast" @@ -33,7 +33,7 @@ gulp.task( }, "clean-cast", "translations-enable-merge-backend", - gulp.parallel("gen-icons-json", "build-translations"), + gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), "copy-static-cast", env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast", "gen-index-cast-prod" diff --git a/build-scripts/gulp/demo.js b/build-scripts/gulp/demo.js index 466ade84b2..25f1670d26 100644 --- a/build-scripts/gulp/demo.js +++ b/build-scripts/gulp/demo.js @@ -20,7 +20,12 @@ gulp.task( }, "clean-demo", "translations-enable-merge-backend", - gulp.parallel("gen-icons-json", "gen-index-demo-dev", "build-translations"), + gulp.parallel( + "gen-icons-json", + "gen-index-demo-dev", + "build-translations", + "build-locale-data" + ), "copy-static-demo", env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo" ) @@ -35,7 +40,7 @@ gulp.task( "clean-demo", // Cast needs to be backwards compatible and older HA has no translations "translations-enable-merge-backend", - gulp.parallel("gen-icons-json", "build-translations"), + gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), "copy-static-demo", env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo", "gen-index-demo-prod" diff --git a/build-scripts/gulp/gallery.js b/build-scripts/gulp/gallery.js index 194a6e532d..2823ad70b1 100644 --- a/build-scripts/gulp/gallery.js +++ b/build-scripts/gulp/gallery.js @@ -51,6 +51,7 @@ gulp.task( gulp.parallel( "gen-icons-json", "build-translations", + "build-locale-data", "gather-gallery-demos" ), "copy-static-gallery", @@ -70,6 +71,7 @@ gulp.task( gulp.parallel( "gen-icons-json", "build-translations", + "build-locale-data", "gather-gallery-demos" ), "copy-static-gallery", diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js index e156c84825..c0f36d02af 100644 --- a/build-scripts/gulp/gather-static.js +++ b/build-scripts/gulp/gather-static.js @@ -22,11 +22,18 @@ function copyTranslations(staticDir) { // Translation output fs.copySync( - polyPath("build-translations/output"), + polyPath("build/translations/output"), staticPath("translations") ); } +function copyLocaleData(staticDir) { + const staticPath = genStaticPath(staticDir); + + // Locale data output + fs.copySync(polyPath("build/locale-data"), staticPath("locale-data")); +} + function copyMdiIcons(staticDir) { const staticPath = genStaticPath(staticDir); @@ -84,6 +91,11 @@ function copyMapPanel(staticDir) { ); } +gulp.task("copy-locale-data", async () => { + const staticDir = paths.app_output_static; + copyLocaleData(staticDir); +}); + gulp.task("copy-translations-app", async () => { const staticDir = paths.app_output_static; copyTranslations(staticDir); @@ -94,6 +106,11 @@ gulp.task("copy-translations-supervisor", async () => { copyTranslations(staticDir); }); +gulp.task("copy-locale-data-supervisor", async () => { + const staticDir = paths.hassio_output_static; + copyLocaleData(staticDir); +}); + gulp.task("copy-static-app", async () => { const staticDir = paths.app_output_static; // Basic static files @@ -103,6 +120,7 @@ gulp.task("copy-static-app", async () => { copyPolyfills(staticDir); copyFonts(staticDir); copyTranslations(staticDir); + copyLocaleData(staticDir); copyMdiIcons(staticDir); // Panel assets @@ -123,6 +141,7 @@ gulp.task("copy-static-demo", async () => { copyMapPanel(paths.demo_output_static); copyFonts(paths.demo_output_static); copyTranslations(paths.demo_output_static); + copyLocaleData(paths.demo_output_static); copyMdiIcons(paths.demo_output_static); }); @@ -137,6 +156,7 @@ gulp.task("copy-static-cast", async () => { copyMapPanel(paths.cast_output_static); copyFonts(paths.cast_output_static); copyTranslations(paths.cast_output_static); + copyLocaleData(paths.cast_output_static); copyMdiIcons(paths.cast_output_static); }); @@ -152,5 +172,6 @@ gulp.task("copy-static-gallery", async () => { copyMapPanel(paths.gallery_output_static); copyFonts(paths.gallery_output_static); copyTranslations(paths.gallery_output_static); + copyLocaleData(paths.gallery_output_static); copyMdiIcons(paths.gallery_output_static); }); diff --git a/build-scripts/gulp/hassio.js b/build-scripts/gulp/hassio.js index 49c5ecd6b6..a21853d37a 100644 --- a/build-scripts/gulp/hassio.js +++ b/build-scripts/gulp/hassio.js @@ -24,6 +24,8 @@ gulp.task( "gen-index-hassio-dev", "build-supervisor-translations", "copy-translations-supervisor", + "build-locale-data", + "copy-locale-data-supervisor", env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio" ) ); @@ -38,6 +40,8 @@ gulp.task( "gen-icons-json", "build-supervisor-translations", "copy-translations-supervisor", + "build-locale-data", + "copy-locale-data-supervisor", env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio", "gen-index-hassio-prod", ...// Don't compress running tests diff --git a/build-scripts/gulp/locale-data.js b/build-scripts/gulp/locale-data.js new file mode 100755 index 0000000000..4c085f99e2 --- /dev/null +++ b/build-scripts/gulp/locale-data.js @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ + +const del = require("del"); +const path = require("path"); +const gulp = require("gulp"); +const fs = require("fs"); +const merge = require("gulp-merge-json"); +const rename = require("gulp-rename"); +const transform = require("gulp-json-transform"); +const paths = require("../paths"); + +const outDir = "build/locale-data"; + +gulp.task("clean-locale-data", () => del([outDir])); + +gulp.task("ensure-locale-data-build-dir", (done) => { + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + done(); +}); + +const modules = { + "intl-relativetimeformat": "RelativeTimeFormat", + "intl-datetimeformat": "DateTimeFormat", + "intl-numberformat": "NumberFormat", +}; + +gulp.task("create-locale-data", (done) => { + const translationMeta = JSON.parse( + fs.readFileSync( + path.join(paths.translations_src, "translationMetadata.json") + ) + ); + Object.entries(modules).forEach(([module, className]) => { + Object.keys(translationMeta).forEach((lang) => { + try { + const localeData = String( + fs.readFileSync( + require.resolve(`@formatjs/${module}/locale-data/${lang}.js`) + ) + ) + .replace( + new RegExp( + `\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`, + "im" + ), + "" + ) + .replace(/\)\s*}/im, ""); + // make sure we have valid JSON + JSON.parse(localeData); + if (!fs.existsSync(path.join(outDir, module))) { + fs.mkdirSync(path.join(outDir, module), { recursive: true }); + } + fs.writeFileSync( + path.join(outDir, `${module}/${lang}.json`), + localeData + ); + } catch (e) { + if (e.code !== "MODULE_NOT_FOUND") { + throw e; + } + } + }); + done(); + }); +}); + +gulp.task( + "build-locale-data", + gulp.series( + "clean-locale-data", + "ensure-locale-data-build-dir", + "create-locale-data" + ) +); diff --git a/build-scripts/gulp/translations.js b/build-scripts/gulp/translations.js index f5228bfa90..f2df996826 100755 --- a/build-scripts/gulp/translations.js +++ b/build-scripts/gulp/translations.js @@ -17,7 +17,7 @@ const paths = require("../paths"); const inFrontendDir = "translations/frontend"; const inBackendDir = "translations/backend"; -const workDir = "build-translations"; +const workDir = "build/translations"; const fullDir = workDir + "/full"; const coreDir = workDir + "/core"; const outDir = workDir + "/output"; @@ -121,7 +121,7 @@ gulp.task("clean-translations", () => del([workDir])); gulp.task("ensure-translations-build-dir", (done) => { if (!fs.existsSync(workDir)) { - fs.mkdirSync(workDir); + fs.mkdirSync(workDir, { recursive: true }); } done(); }); diff --git a/build-scripts/gulp/webpack.js b/build-scripts/gulp/webpack.js index a9ba25da8c..8639296a6e 100644 --- a/build-scripts/gulp/webpack.js +++ b/build-scripts/gulp/webpack.js @@ -148,7 +148,7 @@ gulp.task("webpack-watch-hassio", () => { isProdBuild: false, latestBuild: true, }) - ).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler()); + ).watch({ ignored: /build/, poll: isWsl }, doneHandler()); gulp.watch( path.join(paths.translations_src, "en.json"), diff --git a/src/common/translations/localize.ts b/src/common/translations/localize.ts index c6efe3ea6b..b88aae10f9 100644 --- a/src/common/translations/localize.ts +++ b/src/common/translations/localize.ts @@ -4,6 +4,7 @@ import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-rel import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill"; import IntlMessageFormat from "intl-messageformat"; import { Resources } from "../../types"; +import { getLocalLanguage } from "../../util/hass-translation"; export type LocalizeFunc = (key: string, ...args: any[]) => string; interface FormatType { @@ -15,34 +16,32 @@ export interface FormatsType { time: FormatType; } -const polyfillPluralRules = shouldPolyfillPluralRules(); -const polyfillRelativeTime = shouldPolyfillRelativeTime(); -const polyfillDateTime = shouldPolyfillDateTime(); +const loadedPolyfillLocale = new Set(); const polyfills: Promise[] = []; if (__BUILD__ === "latest") { if (shouldPolyfillLocale()) { polyfills.push(import("@formatjs/intl-locale/polyfill")); } - if (polyfillPluralRules) { + if (shouldPolyfillPluralRules()) { polyfills.push(import("@formatjs/intl-pluralrules/polyfill")); + polyfills.push(import("@formatjs/intl-pluralrules/locale-data/en")); } - if (polyfillRelativeTime) { + if (shouldPolyfillRelativeTime()) { polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill")); } - if (polyfillDateTime) { + if (shouldPolyfillDateTime()) { polyfills.push(import("@formatjs/intl-datetimeformat/polyfill")); } } -let polyfillLoaded = polyfills.length === 0; -export const polyfillsLoaded = polyfillLoaded - ? undefined - : Promise.all(polyfills).then(() => { - polyfillLoaded = true; - // Load English so it becomes the default - return loadPolyfillLocales("en"); - }); +export const polyfillsLoaded = + polyfills.length === 0 + ? undefined + : Promise.all(polyfills).then(() => + // Load the default language + loadPolyfillLocales(getLocalLanguage()) + ); /** * Adapted from Polymer app-localize-behavior. @@ -71,11 +70,11 @@ export const computeLocalize = async ( resources: Resources, formats?: FormatsType ): Promise => { - if (!polyfillLoaded) { + if (polyfillsLoaded) { await polyfillsLoaded; } - loadPolyfillLocales(language); + await loadPolyfillLocales(language); // Everytime any of the parameters change, invalidate the strings cache. cache._localizationCache = {}; @@ -129,28 +128,44 @@ export const computeLocalize = async ( }; export const loadPolyfillLocales = async (language: string) => { - if (!polyfillsLoaded) { + if (loadedPolyfillLocale.has(language)) { return; } - await polyfillsLoaded; + loadedPolyfillLocale.add(language); try { - if (polyfillPluralRules) { - await import( - /* webpackExclude: /.+-.+\.js$/ */ - `@formatjs/intl-pluralrules/locale-data/${language}` + if ( + Intl.NumberFormat && + // @ts-ignore + typeof Intl.NumberFormat.__addLocaleData === "function" + ) { + const result = await fetch( + `/static/locale-data/intl-numberformat/${language}.json` ); + // @ts-ignore + Intl.NumberFormat.__addLocaleData(await result.json()); } - if (polyfillRelativeTime) { - await import( - /* webpackExclude: /.+-.+\.js$/ */ - `@formatjs/intl-relativetimeformat/locale-data/${language}` + if ( + // @ts-expect-error + Intl.RelativeTimeFormat && + // @ts-ignore + typeof Intl.RelativeTimeFormat.__addLocaleData === "function" + ) { + const result = await fetch( + `/static/locale-data/intl-relativetimeformat/${language}.json` ); + // @ts-ignore + Intl.RelativeTimeFormat.__addLocaleData(await result.json()); } - if (polyfillDateTime) { - await import( - /* webpackExclude: /.+-.+\.js$/ */ - `@formatjs/intl-datetimeformat/locale-data/${language}` + if ( + Intl.DateTimeFormat && + // @ts-ignore + typeof Intl.DateTimeFormat.__addLocaleData === "function" + ) { + const result = await fetch( + `/static/locale-data/intl-datetimeformat/${language}.json` ); + // @ts-ignore + Intl.DateTimeFormat.__addLocaleData(await result.json()); } } catch (_e) { // Ignore diff --git a/src/resources/translations-metadata.ts b/src/resources/translations-metadata.ts index 0957a46cbc..2be35b5487 100644 --- a/src/resources/translations-metadata.ts +++ b/src/resources/translations-metadata.ts @@ -1,4 +1,4 @@ -import * as translationMetadata_ from "../../build-translations/translationMetadata.json"; +import * as translationMetadata_ from "../../build/translations/translationMetadata.json"; import { TranslationMetadata } from "../types.js"; export const translationMetadata = (translationMetadata_ as any)