diff --git a/tutorials/js.md b/tutorials/js.md index 0cf42a1614e..64a5dbde7e4 100644 --- a/tutorials/js.md +++ b/tutorials/js.md @@ -10,7 +10,7 @@ _We are still working on improving the user experience of using ICU4X from other Similar to C++, the JS APIs mirror the Rust code in the `icu_capi` crate, which can be explored on [docs.rs][rust-docs], though the precise types used may be different. -See [`ffi/npm/examples/wasm-demo`] for an NPM package that uses the ICU4X package. You can run it using `npm run start`. +See [`docs/tutorials/npm`] for an NPM package that uses the ICU4X package. You can run it using `npm run start`. We hope to fill in these docs over time with more examples. diff --git a/tutorials/npm/.gitignore b/tutorials/npm/.gitignore index a261f291755..2e51cd4a348 100644 --- a/tutorials/npm/.gitignore +++ b/tutorials/npm/.gitignore @@ -1 +1,2 @@ dist/* +gen_hash.txt \ No newline at end of file diff --git a/tutorials/npm/gen.sh b/tutorials/npm/gen.sh new file mode 100644 index 00000000000..a05095e5124 --- /dev/null +++ b/tutorials/npm/gen.sh @@ -0,0 +1,135 @@ +#!/bin/bash + +locales=("ja" "th" "zh" "bn" "und" "de" "en") + +if [ ! -d "dist" ]; then + mkdir dist + echo "dist folder created." +fi + +for locale in "${locales[@]}"; do + cargo run --package icu_datagen -- --keys \ + "calendar/japanese@1" \ + "calendar/japanext@1" \ + "datetime/buddhist/datelengths@1" \ + "datetime/buddhist/datesymbols@1" \ + "datetime/chinese/datelengths@1" \ + "datetime/chinese/datesymbols@1" \ + "datetime/coptic/datelengths@1" \ + "datetime/coptic/datesymbols@1" \ + "datetime/dangi/datelengths@1" \ + "datetime/dangi/datesymbols@1" \ + "datetime/ethiopic/datelengths@1" \ + "datetime/ethiopic/datesymbols@1" \ + "datetime/gregory/datelengths@1" \ + "datetime/gregory/datesymbols@1" \ + "datetime/hebrew/datelengths@1" \ + "datetime/hebrew/datesymbols@1" \ + "datetime/indian/datelengths@1" \ + "datetime/indian/datesymbols@1" \ + "datetime/islamic/datelengths@1" \ + "datetime/islamic/datesymbols@1" \ + "datetime/japanese/datelengths@1" \ + "datetime/japanese/datesymbols@1" \ + "datetime/japanext/datelengths@1" \ + "datetime/japanext/datesymbols@1" \ + "datetime/patterns/buddhist/date@1" \ + "datetime/patterns/chinese/date@1" \ + "datetime/patterns/coptic/date@1" \ + "datetime/patterns/dangi/date@1" \ + "datetime/patterns/datetime@1" \ + "datetime/patterns/ethiopic/date@1" \ + "datetime/patterns/gregory/date@1" \ + "datetime/patterns/hebrew/date@1" \ + "datetime/patterns/indian/date@1" \ + "datetime/patterns/islamic/date@1" \ + "datetime/patterns/japanese/date@1" \ + "datetime/patterns/japanext/date@1" \ + "datetime/patterns/persian/date@1" \ + "datetime/patterns/roc/date@1" \ + "datetime/patterns/time@1" \ + "datetime/persian/datelengths@1" \ + "datetime/persian/datesymbols@1" \ + "datetime/roc/datelengths@1" \ + "datetime/roc/datesymbols@1" \ + "datetime/skeletons@1" \ + "datetime/symbols/buddhist/months@1" \ + "datetime/symbols/buddhist/years@1" \ + "datetime/symbols/chinese/months@1" \ + "datetime/symbols/chinese/years@1" \ + "datetime/symbols/coptic/months@1" \ + "datetime/symbols/coptic/years@1" \ + "datetime/symbols/dangi/months@1" \ + "datetime/symbols/dangi/years@1" \ + "datetime/symbols/dayperiods@1" \ + "datetime/symbols/ethiopic/months@1" \ + "datetime/symbols/ethiopic/years@1" \ + "datetime/symbols/gregory/months@1" \ + "datetime/symbols/gregory/years@1" \ + "datetime/symbols/hebrew/months@1" \ + "datetime/symbols/hebrew/years@1" \ + "datetime/symbols/indian/months@1" \ + "datetime/symbols/indian/years@1" \ + "datetime/symbols/islamic/months@1" \ + "datetime/symbols/islamic/years@1" \ + "datetime/symbols/japanese/months@1" \ + "datetime/symbols/japanese/years@1" \ + "datetime/symbols/japanext/months@1" \ + "datetime/symbols/japanext/years@1" \ + "datetime/symbols/persian/months@1" \ + "datetime/symbols/persian/years@1" \ + "datetime/symbols/roc/months@1" \ + "datetime/symbols/roc/years@1" \ + "datetime/symbols/weekdays@1" \ + "datetime/timelengths@1" \ + "datetime/timesymbols@1" \ + "datetime/week_data@1" \ + "decimal/symbols@1" \ + "fallback/likelysubtags@1" \ + "fallback/parents@1" \ + "fallback/supplement/co@1" \ + "list/and@1" \ + "list/or@1" \ + "list/unit@1" \ + "locid_transform/aliases@1" \ + "locid_transform/likelysubtags@1" \ + "locid_transform/likelysubtags_ext@1" \ + "locid_transform/likelysubtags_l@1" \ + "locid_transform/likelysubtags_sr@1" \ + "locid_transform/script_dir@1" \ + "plurals/cardinal@1" \ + "plurals/ordinal@1" \ + "plurals/ranges@1" \ + "segmenter/dictionary/w_auto@1" \ + "segmenter/dictionary/wl_ext@1" \ + "segmenter/grapheme@1" \ + "segmenter/line@1" \ + "segmenter/lstm/wl_auto@1" \ + "segmenter/sentence@1" \ + "segmenter/word@1" \ + "time_zone/bcp47_to_iana@1" \ + "time_zone/exemplar_cities@1" \ + "time_zone/formats@1" \ + "time_zone/generic_long@1" \ + "time_zone/generic_short@1" \ + "time_zone/iana_to_bcp47@1" \ + "time_zone/metazone_period@1" \ + "time_zone/specific_long@1" \ + "time_zone/specific_short@1" \ + --fallback preresolved --locales $locale --format blob2 --out dist/$locale.postcard --overwrite +done + +ts_content="const locales: string[] = [" + +for locale in "${locales[@]}"; do + ts_content+="\"$locale\", " +done + +ts_content=${ts_content%, } +ts_content+="];" + +ts_content+="\nexport default locales;" + +echo "$ts_content" > dist/locales.ts + +echo "locales.ts file has been generated." \ No newline at end of file diff --git a/tutorials/npm/gen_helper.js b/tutorials/npm/gen_helper.js new file mode 100644 index 00000000000..2e8c68ab5dd --- /dev/null +++ b/tutorials/npm/gen_helper.js @@ -0,0 +1,47 @@ +import fs from 'fs'; +import crypto from 'crypto'; +import { spawn } from 'child_process'; + +const genShFile = 'gen.sh'; +const hashFile = 'gen_hash.txt'; + +function calculateHash(filePath) { + const hash = crypto.createHash('sha256'); + const fileData = fs.readFileSync(filePath); + hash.update(fileData); + return hash.digest('hex'); +} + +function runGenSh() { + console.log('Running gen.sh...'); + const genShProcess = spawn('sh', [genShFile]); + + genShProcess.stdout.on('data', (data) => { + console.log(data.toString()); + }); + + genShProcess.stderr.on('data', (data) => { + console.error(data.toString()); + }); + + genShProcess.on('close', (code) => { + if (code !== 0) { + console.error(`gen.sh exited with code ${code}`); + } + }); +} + +try { + const currentHash = calculateHash(genShFile); + const previousHash = fs.existsSync(hashFile) ? fs.readFileSync(hashFile, 'utf-8') : null; + + if (currentHash !== previousHash) { + runGenSh(); + fs.writeFileSync(hashFile, currentHash); + console.log('gen.sh has been updated. Output regenerated.'); + } else { + console.log('gen.sh has not changed. Skipping execution.'); + } +} catch (error) { + console.error('An error occurred:', error); +} diff --git a/tutorials/npm/package.json b/tutorials/npm/package.json index 9a4e6321e24..28c4b2e6bf5 100644 --- a/tutorials/npm/package.json +++ b/tutorials/npm/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "clean": "rm dist/*", - "build": "webpack", + "build": "node gen_helper.js && webpack", "start": "webpack serve --mode development --port 12349", "tsc": "tsc -p ." }, diff --git a/tutorials/npm/src/ts/app.ts b/tutorials/npm/src/ts/app.ts index c0d3a7a56d8..b65152ec781 100644 --- a/tutorials/npm/src/ts/app.ts +++ b/tutorials/npm/src/ts/app.ts @@ -1,4 +1,4 @@ -import { ICU4XDataProvider } from 'icu4x'; +import { DataProviderManager } from './data-provider-manager'; import * as fdf from './fixed-decimal'; import * as dtf from './date-time'; import * as seg from './segmenter'; @@ -8,9 +8,11 @@ import 'bootstrap/js/dist/dropdown'; import 'bootstrap/js/dist/collapse'; (async function init() { - const dataProvider = ICU4XDataProvider.create_compiled(); - fdf.setup(dataProvider); - dtf.setup(dataProvider); - seg.setup(dataProvider); + const dataManager = await DataProviderManager.create(); + + fdf.setup(dataManager); + dtf.setup(dataManager); + seg.setup(dataManager); + (document.querySelector("#bigspinner") as HTMLElement).style.display = "none"; })() \ No newline at end of file diff --git a/tutorials/npm/src/ts/data-provider-manager.ts b/tutorials/npm/src/ts/data-provider-manager.ts new file mode 100644 index 00000000000..f0164a316fe --- /dev/null +++ b/tutorials/npm/src/ts/data-provider-manager.ts @@ -0,0 +1,87 @@ +import { + ICU4XDataProvider, + ICU4XLocale, + ICU4XLocaleFallbacker, +} from 'icu4x'; + +export class DataProviderManager { + + private dataProvider: ICU4XDataProvider; + private loadedLocales: Set; + + private constructor() { + this.loadedLocales = new Set(); + } + + public static async create(): Promise { + const manager = new DataProviderManager(); + await manager.init(); + return manager; + } + + private async init() { + + const enFilePath = 'dist/en.postcard'; + let enProvider = await this.createDataProviderFromBlob(enFilePath); + this.loadedLocales.add(ICU4XLocale.create_from_string("en")); + const unFilePath = 'dist/en.postcard'; + let unProvider = await this.createDataProviderFromBlob(unFilePath); + let fallbacker = ICU4XLocaleFallbacker.create(unProvider); + enProvider.enable_locale_fallback_with(fallbacker); + this.dataProvider = enProvider; + } + + private async createDataProviderFromBlob(filePath: string): Promise { + const blob = await this.readBlobFromFile(filePath); + const arrayBuffer = await blob.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + const newDataProvider = ICU4XDataProvider.create_from_byte_slice(uint8Array); + return newDataProvider; + } + + private async readBlobFromFile(path: string): Promise { + const response = await fetch(path); + if (!response.ok) { + throw new Error(`Failed to fetch file: ${response.statusText}`); + } + const blob = await response.blob(); + return blob; + } + + public supportsLocale(locid: string): boolean { + const locales = this.getLoadedLocales(); + const localesFinal: string[] = []; + locales.forEach((item: ICU4XLocale) => { + localesFinal.push(item.to_string) + }) + const loadedLocales = new Set(localesFinal); + return loadedLocales.has(locid); + } + + public async loadLocale(newLocale: string): Promise { + const icu4xLocale = ICU4XLocale.create_from_string(newLocale); + const newFilePath = `dist/${newLocale}.postcard`; + let newProvider = await this.createDataProviderFromBlob(newFilePath); + await this.dataProvider.fork_by_locale(newProvider); + this.loadedLocales.add(ICU4XLocale.create_from_string(newLocale)); + return this.dataProvider; + } + + public async getSegmenterProviderLocale(): Promise { + const segmenterLocale = ['ja', 'zh', 'th']; + let segmenterProvider: ICU4XDataProvider; + for (let i = 0; i < segmenterLocale.length; i++) { + segmenterProvider = await this.loadLocale(segmenterLocale[i]); + } + return segmenterProvider; + } + + public getLoadedLocales() { + return this.loadedLocales; + } + + public getDataProvider(): ICU4XDataProvider { + return this.dataProvider; + } + +} \ No newline at end of file diff --git a/tutorials/npm/src/ts/date-time.ts b/tutorials/npm/src/ts/date-time.ts index 5f84f8f4a60..cfeeafd4152 100644 --- a/tutorials/npm/src/ts/date-time.ts +++ b/tutorials/npm/src/ts/date-time.ts @@ -1,9 +1,24 @@ -import { ICU4XDataProvider, ICU4XDateLength, ICU4XDateTime, ICU4XDateTimeFormatter, ICU4XLocale, ICU4XTimeLength, ICU4XCalendar } from "icu4x"; -import { Ok, Result, result, unwrap } from "./index"; +import { + ICU4XDataProvider, + ICU4XDateLength, + ICU4XDateTime, + ICU4XDateTimeFormatter, + ICU4XLocale, + ICU4XTimeLength, + ICU4XCalendar +} from "icu4x"; +import { DataProviderManager } from './data-provider-manager'; +import { + Ok, + Result, + result, + unwrap +} from "./index"; export class DateTimeDemo { #displayFn: (formatted: string) => void; #dataProvider: ICU4XDataProvider; + #dataProviderManager: DataProviderManager; #localeStr: string; #calendarStr: string; @@ -16,12 +31,13 @@ export class DateTimeDemo { #formatter: Result; #dateTime: Result | null; - constructor(displayFn: (formatted: string) => void, dataProvider: ICU4XDataProvider) { + constructor(displayFn: (formatted: string) => void, dataProviderManager: DataProviderManager) { + this.#displayFn = displayFn; - this.#dataProvider = dataProvider; - + this.#dataProvider = dataProviderManager.getDataProvider(); + this.#dataProviderManager = dataProviderManager; this.#locale = Ok(ICU4XLocale.create_from_string("en")); - this.#calendar = Ok(ICU4XCalendar.create_for_locale(dataProvider, unwrap(this.#locale))); + this.#calendar = Ok(ICU4XCalendar.create_for_locale(this.#dataProvider, unwrap(this.#locale))); this.#dateLength = ICU4XDateLength.Short; this.#timeLength = ICU4XTimeLength.Short; this.#dateTime = null; @@ -37,9 +53,18 @@ export class DateTimeDemo { this.#updateFormatter(); } - setLocale(locid: string): void { - this.#localeStr = locid; - this.#updateLocaleAndCalendar(); + async setLocale(locid: string): Promise { + this.#locale = result(() => ICU4XLocale.create_from_string(locid)); + if (this.#locale.ok == true) { + if(!this.#dataProviderManager.supportsLocale(locid)){ + await this.updateProvider(this.#locale.value); + } + } + this.#updateFormatter() + } + + async updateProvider(newLocale: ICU4XLocale) { + await this.#dataProviderManager.loadLocale(newLocale.to_string()); this.#updateFormatter(); } diff --git a/tutorials/npm/src/ts/fixed-decimal.ts b/tutorials/npm/src/ts/fixed-decimal.ts index c604d3d0130..e111c3fbbf5 100644 --- a/tutorials/npm/src/ts/fixed-decimal.ts +++ b/tutorials/npm/src/ts/fixed-decimal.ts @@ -1,33 +1,57 @@ -import { ICU4XDataProvider, ICU4XFixedDecimal, ICU4XFixedDecimalFormatter, ICU4XFixedDecimalGroupingStrategy, ICU4XLocale } from "icu4x"; -import { Result, Ok, result, unwrap } from './index'; +import { + ICU4XDataProvider, + ICU4XFixedDecimal, + ICU4XFixedDecimalFormatter, + ICU4XFixedDecimalGroupingStrategy, + ICU4XLocale +} from "icu4x"; +import { DataProviderManager } from './data-provider-manager'; +import { + Result, + Ok, + result, + unwrap, +} from './index'; export class FixedDecimalDemo { #displayFn: (formatted: string) => void; #dataProvider: ICU4XDataProvider; + #dataProviderManager: DataProviderManager; #locale: Result; #groupingStrategy: ICU4XFixedDecimalGroupingStrategy; #formatter: Result; #fixedDecimal: Result | null; - constructor(displayFn: (formatted: string) => void, dataProvider: ICU4XDataProvider) { + constructor(displayFn: (formatted: string) => void, dataProviderManager: DataProviderManager) { this.#displayFn = displayFn; - this.#dataProvider = dataProvider; + this.#dataProvider = dataProviderManager.getDataProvider(); + this.#dataProviderManager = dataProviderManager; this.#locale = Ok(ICU4XLocale.create_from_string("en")); this.#groupingStrategy = ICU4XFixedDecimalGroupingStrategy.Auto; this.#fixedDecimal = null; - this.#updateFormatter() + this.#updateFormatter(); } - setLocale(locid: string): void { + async setLocale(locid: string): Promise { this.#locale = result(() => ICU4XLocale.create_from_string(locid)); + if (this.#locale.ok == true) { + if(!this.#dataProviderManager.supportsLocale(locid)){ + await this.updateProvider(this.#locale.value); + } + } this.#updateFormatter() } + async updateProvider(newLocale: ICU4XLocale): Promise { + this.#dataProvider = await this.#dataProviderManager.loadLocale(newLocale.to_string()); + this.#updateFormatter(); + } + setGroupingStrategy(strategy: string): void { this.#groupingStrategy = ICU4XFixedDecimalGroupingStrategy[strategy]; - this.#updateFormatter() + this.#updateFormatter(); } setFixedDecimal(digits: string): void { @@ -39,7 +63,7 @@ export class FixedDecimalDemo { this.#formatter = result(() => ICU4XFixedDecimalFormatter.create_with_grouping_strategy( this.#dataProvider, unwrap(this.#locale), - this.#groupingStrategy, + this.#groupingStrategy )); this.#render(); } @@ -80,16 +104,16 @@ export function setup(dataProvider: ICU4XDataProvider): void { } }); - for (let btn of document.querySelectorAll('input[name="fdf-locale"]')) { + for (let btn of document.querySelectorAll < HTMLInputElement | null > ('input[name="fdf-locale"]')) { if (btn?.value !== 'other') { btn.addEventListener('click', () => fixedDecimalDemo.setLocale(btn.value)); } } - for (let btn of document.querySelectorAll('input[name="fdf-grouping"]')) { + for (let btn of document.querySelectorAll < HTMLInputElement | null > ('input[name="fdf-grouping"]')) { btn?.addEventListener('click', () => fixedDecimalDemo.setGroupingStrategy(btn.value)); } const inputDecimal = document.getElementById('fdf-input') as HTMLTextAreaElement | null; inputDecimal?.addEventListener('input', () => fixedDecimalDemo.setFixedDecimal(inputDecimal.value)); -} +} \ No newline at end of file diff --git a/tutorials/npm/src/ts/segmenter.ts b/tutorials/npm/src/ts/segmenter.ts index 2a1efe6407e..f6d861a9fcf 100644 --- a/tutorials/npm/src/ts/segmenter.ts +++ b/tutorials/npm/src/ts/segmenter.ts @@ -1,17 +1,20 @@ import { ICU4XDataProvider, ICU4XWordSegmenter } from "icu4x"; +import { DataProviderManager } from './data-provider-manager'; export class SegmenterDemo { #displayFn: (formatted: string) => void; #dataProvider: ICU4XDataProvider; + #dataProviderManager: DataProviderManager; #segmenter: ICU4XWordSegmenter; #model: string; #text: string; - constructor(displayFn: (formatted: string) => void, dataProvider: ICU4XDataProvider) { + constructor(displayFn: (formatted: string) => void, dataProviderManager: DataProviderManager) { this.#displayFn = displayFn; - this.#dataProvider = dataProvider; - + this.#dataProvider = dataProviderManager.getDataProvider(); + this.#dataProviderManager = dataProviderManager; + this.#model = "Auto"; this.#text = ""; this.#updateSegmenter(); @@ -27,7 +30,8 @@ export class SegmenterDemo { this.#render(); } - #updateSegmenter(): void { + async #updateSegmenter(): Promise { + this.#dataProvider = await this.#dataProviderManager.getSegmenterProviderLocale(); if (this.#model === "Auto") { this.#segmenter = ICU4XWordSegmenter.create_auto(this.#dataProvider); } else if (this.#model === "LSTM") {