@nyaruka/temba-components 0.156.7 → 0.156.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/temba-components.js +554 -549
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/flow/Editor.ts +360 -3
package/package.json
CHANGED
package/src/flow/Editor.ts
CHANGED
|
@@ -22,12 +22,15 @@ import {
|
|
|
22
22
|
} from '../utils';
|
|
23
23
|
import { TEMBA_COMPONENTS_VERSION } from '../version';
|
|
24
24
|
import {
|
|
25
|
+
getLanguageDisplayName,
|
|
25
26
|
getNodeBounds,
|
|
26
27
|
calculateReflowPositions,
|
|
27
28
|
NodeBounds,
|
|
28
29
|
snapToGrid
|
|
29
30
|
} from './utils';
|
|
30
31
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
32
|
+
import { getTranslatableCategoriesForNode } from './categoryLocalization';
|
|
33
|
+
import { PRIMARY_LANGUAGE_OPTION_VALUE } from './EditorToolbar';
|
|
31
34
|
import { calculateLayeredLayout, placeStickyNotes } from './reflow';
|
|
32
35
|
import type { RevisionsWindow } from './RevisionsWindow';
|
|
33
36
|
|
|
@@ -77,6 +80,22 @@ export interface SelectionBox {
|
|
|
77
80
|
endY: number;
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
type TranslationType = 'property' | 'category';
|
|
84
|
+
|
|
85
|
+
interface TranslationEntry {
|
|
86
|
+
uuid: string;
|
|
87
|
+
type: TranslationType;
|
|
88
|
+
attribute: string;
|
|
89
|
+
from: string;
|
|
90
|
+
to: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface TranslationBundle {
|
|
94
|
+
nodeUuid: string;
|
|
95
|
+
actionUuid?: string;
|
|
96
|
+
translations: TranslationEntry[];
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
export type ToolbarAction =
|
|
81
100
|
| { action: 'view-change'; view: 'flow' | 'table' }
|
|
82
101
|
| { action: 'zoom-in' }
|
|
@@ -84,7 +103,8 @@ export type ToolbarAction =
|
|
|
84
103
|
| { action: 'zoom-to-fit' }
|
|
85
104
|
| { action: 'zoom-to-full' }
|
|
86
105
|
| { action: 'revisions' }
|
|
87
|
-
| { action: 'search' }
|
|
106
|
+
| { action: 'search' }
|
|
107
|
+
| { action: 'language-change'; isPrimary?: boolean; languageCode?: string };
|
|
88
108
|
const EMPTY_FLOW_ISSUES: FlowIssue[] = [];
|
|
89
109
|
|
|
90
110
|
// How long the pending-changes auto-save countdown runs (in ms).
|
|
@@ -1736,6 +1756,287 @@ export class Editor extends RapidElement {
|
|
|
1736
1756
|
zustand.getState().setLanguageCode(languageCode);
|
|
1737
1757
|
}
|
|
1738
1758
|
|
|
1759
|
+
private getAvailableLanguages(): Array<{ code: string; name: string }> {
|
|
1760
|
+
// Use languages from workspace if available
|
|
1761
|
+
if (this.workspace?.languages && this.workspace.languages.length > 0) {
|
|
1762
|
+
return this.workspace.languages
|
|
1763
|
+
.map((code) => ({ code, name: getLanguageDisplayName(code) }))
|
|
1764
|
+
.filter((lang) => lang.code && lang.name);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// Fall back to flow definition languages if available
|
|
1768
|
+
if (
|
|
1769
|
+
this.definition?._ui?.languages &&
|
|
1770
|
+
this.definition._ui.languages.length > 0
|
|
1771
|
+
) {
|
|
1772
|
+
return this.definition._ui.languages.map((lang: any) => ({
|
|
1773
|
+
code: typeof lang === 'string' ? lang : lang.iso || lang.code,
|
|
1774
|
+
name: typeof lang === 'string' ? lang : lang.name
|
|
1775
|
+
}));
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// No languages available
|
|
1779
|
+
return [];
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
private getLocalizationLanguages(): Array<{ code: string; name: string }> {
|
|
1783
|
+
if (!this.definition) {
|
|
1784
|
+
return [];
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
const baseLanguage = this.definition.language;
|
|
1788
|
+
return this.getAvailableLanguages().filter(
|
|
1789
|
+
(lang) => lang.code !== baseLanguage
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
private getLocalizationProgress(languageCode: string): {
|
|
1794
|
+
total: number;
|
|
1795
|
+
localized: number;
|
|
1796
|
+
} {
|
|
1797
|
+
if (
|
|
1798
|
+
!this.definition ||
|
|
1799
|
+
!languageCode ||
|
|
1800
|
+
languageCode === this.definition.language
|
|
1801
|
+
) {
|
|
1802
|
+
return { total: 0, localized: 0 };
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const bundles = this.buildTranslationBundles(languageCode);
|
|
1806
|
+
return this.getTranslationCounts(bundles);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
private getLanguageLocalization(languageCode: string): Record<string, any> {
|
|
1810
|
+
if (!this.definition?.localization) {
|
|
1811
|
+
return {};
|
|
1812
|
+
}
|
|
1813
|
+
return this.definition.localization[languageCode] || {};
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
private buildTranslationBundles(
|
|
1817
|
+
languageCode: string = this.languageCode
|
|
1818
|
+
): TranslationBundle[] {
|
|
1819
|
+
if (
|
|
1820
|
+
!this.definition ||
|
|
1821
|
+
!languageCode ||
|
|
1822
|
+
languageCode === this.definition.language
|
|
1823
|
+
) {
|
|
1824
|
+
return [];
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
const languageLocalization = this.getLanguageLocalization(languageCode);
|
|
1828
|
+
const bundles: TranslationBundle[] = [];
|
|
1829
|
+
|
|
1830
|
+
this.definition.nodes.forEach((node) => {
|
|
1831
|
+
node.actions?.forEach((action) => {
|
|
1832
|
+
const config = ACTION_CONFIG[action.type];
|
|
1833
|
+
if (!config?.localizable || config.localizable.length === 0) {
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// For send_msg actions, only count 'text' for progress tracking
|
|
1838
|
+
// (quick_replies and attachments are still localizable but don't count toward progress)
|
|
1839
|
+
const localizableKeys =
|
|
1840
|
+
action.type === 'send_msg'
|
|
1841
|
+
? config.localizable.filter((key) => key === 'text')
|
|
1842
|
+
: config.localizable;
|
|
1843
|
+
|
|
1844
|
+
const translations = this.findTranslations(
|
|
1845
|
+
'property',
|
|
1846
|
+
action.uuid,
|
|
1847
|
+
localizableKeys,
|
|
1848
|
+
action,
|
|
1849
|
+
languageLocalization
|
|
1850
|
+
);
|
|
1851
|
+
|
|
1852
|
+
if (translations.length > 0) {
|
|
1853
|
+
bundles.push({
|
|
1854
|
+
nodeUuid: node.uuid,
|
|
1855
|
+
actionUuid: action.uuid,
|
|
1856
|
+
translations
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
const nodeUI = this.definition._ui?.nodes?.[node.uuid];
|
|
1862
|
+
const nodeType = nodeUI?.type;
|
|
1863
|
+
if (!nodeType) {
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// Include rule (case argument) translations when localizeRules is set
|
|
1868
|
+
if (nodeUI?.config?.localizeRules && node.router?.cases?.length) {
|
|
1869
|
+
const ruleTranslations = node.router.cases
|
|
1870
|
+
.filter((c) => c.arguments?.length > 0 && c.arguments.some((a) => a))
|
|
1871
|
+
.flatMap((c) =>
|
|
1872
|
+
this.findTranslations(
|
|
1873
|
+
'property',
|
|
1874
|
+
c.uuid,
|
|
1875
|
+
['arguments'],
|
|
1876
|
+
c,
|
|
1877
|
+
languageLocalization
|
|
1878
|
+
)
|
|
1879
|
+
);
|
|
1880
|
+
|
|
1881
|
+
if (ruleTranslations.length > 0) {
|
|
1882
|
+
bundles.push({
|
|
1883
|
+
nodeUuid: node.uuid,
|
|
1884
|
+
translations: ruleTranslations
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const nodeConfig = NODE_CONFIG[nodeType];
|
|
1890
|
+
if (
|
|
1891
|
+
nodeUI?.config?.localizeCategories &&
|
|
1892
|
+
nodeConfig?.localizable === 'categories' &&
|
|
1893
|
+
node.router?.categories?.length
|
|
1894
|
+
) {
|
|
1895
|
+
const translatableCategories = getTranslatableCategoriesForNode(
|
|
1896
|
+
nodeType,
|
|
1897
|
+
node.router.categories
|
|
1898
|
+
);
|
|
1899
|
+
const categoryTranslations = translatableCategories.flatMap(
|
|
1900
|
+
(category) =>
|
|
1901
|
+
this.findTranslations(
|
|
1902
|
+
'category',
|
|
1903
|
+
category.uuid,
|
|
1904
|
+
['name'],
|
|
1905
|
+
category,
|
|
1906
|
+
languageLocalization
|
|
1907
|
+
)
|
|
1908
|
+
);
|
|
1909
|
+
|
|
1910
|
+
if (categoryTranslations.length > 0) {
|
|
1911
|
+
bundles.push({
|
|
1912
|
+
nodeUuid: node.uuid,
|
|
1913
|
+
translations: categoryTranslations
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
return bundles;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
private findTranslations(
|
|
1923
|
+
type: TranslationType,
|
|
1924
|
+
uuid: string,
|
|
1925
|
+
localizeableKeys: string[],
|
|
1926
|
+
source: any,
|
|
1927
|
+
localization: Record<string, any>
|
|
1928
|
+
): TranslationEntry[] {
|
|
1929
|
+
const translations: TranslationEntry[] = [];
|
|
1930
|
+
|
|
1931
|
+
localizeableKeys.forEach((attribute) => {
|
|
1932
|
+
if (attribute === 'quick_replies') {
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
const pathSegments = attribute.split('.');
|
|
1937
|
+
let from: any = source;
|
|
1938
|
+
let to: any = [];
|
|
1939
|
+
|
|
1940
|
+
while (pathSegments.length > 0 && from) {
|
|
1941
|
+
if (from.uuid) {
|
|
1942
|
+
to = localization[from.uuid];
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
const path = pathSegments.shift();
|
|
1946
|
+
if (!path) {
|
|
1947
|
+
break;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
if (to) {
|
|
1951
|
+
to = to[path];
|
|
1952
|
+
}
|
|
1953
|
+
from = from[path];
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
if (!from) {
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const fromValue = this.formatTranslationValue(from);
|
|
1961
|
+
if (!fromValue) {
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
const toValue = to ? this.formatTranslationValue(to) : null;
|
|
1966
|
+
|
|
1967
|
+
translations.push({
|
|
1968
|
+
uuid,
|
|
1969
|
+
type,
|
|
1970
|
+
attribute,
|
|
1971
|
+
from: fromValue,
|
|
1972
|
+
to: toValue
|
|
1973
|
+
});
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
return translations;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
private formatTranslationValue(value: any): string | null {
|
|
1980
|
+
if (value === null || value === undefined) {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
if (Array.isArray(value)) {
|
|
1985
|
+
const normalized = value
|
|
1986
|
+
.map((entry) => this.formatTranslationValue(entry))
|
|
1987
|
+
.filter((entry) => !!entry) as string[];
|
|
1988
|
+
return normalized.length > 0 ? normalized.join(', ') : null;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (typeof value === 'object') {
|
|
1992
|
+
if ('name' in value && value.name) {
|
|
1993
|
+
return String(value.name);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
if ('arguments' in value && Array.isArray(value.arguments)) {
|
|
1997
|
+
return value.arguments.join(' ');
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
if (typeof value === 'number') {
|
|
2004
|
+
return value.toString();
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
if (typeof value === 'string') {
|
|
2008
|
+
const trimmed = value.trim();
|
|
2009
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
private getTranslationCounts(bundles: TranslationBundle[]): {
|
|
2016
|
+
total: number;
|
|
2017
|
+
localized: number;
|
|
2018
|
+
} {
|
|
2019
|
+
return bundles.reduce(
|
|
2020
|
+
(counts, bundle) => {
|
|
2021
|
+
bundle.translations.forEach((translation) => {
|
|
2022
|
+
counts.total += 1;
|
|
2023
|
+
if (translation.to && translation.to.trim().length > 0) {
|
|
2024
|
+
counts.localized += 1;
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
return counts;
|
|
2028
|
+
},
|
|
2029
|
+
{ total: 0, localized: 0 }
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
private hasAnyNodeWithLocalizeCategories(): boolean {
|
|
2034
|
+
if (!this.definition?._ui?.nodes) return false;
|
|
2035
|
+
return Object.values(this.definition._ui.nodes).some(
|
|
2036
|
+
(nodeUI: any) => nodeUI?.config?.localizeCategories
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1739
2040
|
disconnectedCallback(): void {
|
|
1740
2041
|
super.disconnectedCallback();
|
|
1741
2042
|
this.zoomManager.teardownLoupe();
|
|
@@ -1892,7 +2193,7 @@ export class Editor extends RapidElement {
|
|
|
1892
2193
|
search.definition = this.definition;
|
|
1893
2194
|
search.languageCode = this.languageCode || '';
|
|
1894
2195
|
search.scope = this.showMessageTable ? 'table' : 'flow';
|
|
1895
|
-
search.includeCategories =
|
|
2196
|
+
search.includeCategories = this.isTranslating && this.hasAnyNodeWithLocalizeCategories();
|
|
1896
2197
|
search.show();
|
|
1897
2198
|
}
|
|
1898
2199
|
|
|
@@ -3609,6 +3910,50 @@ export class Editor extends RapidElement {
|
|
|
3609
3910
|
}
|
|
3610
3911
|
|
|
3611
3912
|
private renderToolbarElement(): TemplateResult {
|
|
3913
|
+
const languages = this.getLocalizationLanguages();
|
|
3914
|
+
const availableLanguages = this.getAvailableLanguages();
|
|
3915
|
+
const baseLanguage = this.definition?.language;
|
|
3916
|
+
const baseLanguageName =
|
|
3917
|
+
availableLanguages.find((lang) => lang.code === baseLanguage)?.name ||
|
|
3918
|
+
baseLanguage ||
|
|
3919
|
+
'Primary language';
|
|
3920
|
+
const isBaseSelected =
|
|
3921
|
+
!this.languageCode ||
|
|
3922
|
+
this.languageCode === baseLanguage ||
|
|
3923
|
+
!languages.some((lang) => lang.code === this.languageCode);
|
|
3924
|
+
const activeLanguage = !isBaseSelected
|
|
3925
|
+
? languages.find((lang) => lang.code === this.languageCode)
|
|
3926
|
+
: null;
|
|
3927
|
+
const currentLanguage = activeLanguage || {
|
|
3928
|
+
code: baseLanguage || '',
|
|
3929
|
+
name: baseLanguageName
|
|
3930
|
+
};
|
|
3931
|
+
const progress = this.getLocalizationProgress(
|
|
3932
|
+
isBaseSelected ? '' : this.languageCode
|
|
3933
|
+
);
|
|
3934
|
+
const percent = Math.round(
|
|
3935
|
+
(progress.localized / Math.max(progress.total, 1)) * 100
|
|
3936
|
+
);
|
|
3937
|
+
const languageOptions = [
|
|
3938
|
+
{
|
|
3939
|
+
name: baseLanguageName,
|
|
3940
|
+
value: PRIMARY_LANGUAGE_OPTION_VALUE
|
|
3941
|
+
},
|
|
3942
|
+
...languages.map((lang) => {
|
|
3943
|
+
const localizationProgress = this.getLocalizationProgress(lang.code);
|
|
3944
|
+
const localizationPercent = Math.round(
|
|
3945
|
+
(localizationProgress.localized /
|
|
3946
|
+
Math.max(localizationProgress.total, 1)) *
|
|
3947
|
+
100
|
|
3948
|
+
);
|
|
3949
|
+
return {
|
|
3950
|
+
name: lang.name,
|
|
3951
|
+
value: lang.code,
|
|
3952
|
+
percent: localizationPercent
|
|
3953
|
+
};
|
|
3954
|
+
})
|
|
3955
|
+
];
|
|
3956
|
+
|
|
3612
3957
|
return html`
|
|
3613
3958
|
<temba-editor-toolbar
|
|
3614
3959
|
?message-view=${this.showMessageTable}
|
|
@@ -3618,6 +3963,11 @@ export class Editor extends RapidElement {
|
|
|
3618
3963
|
?revisions-active=${!this.revisionsWindowHidden}
|
|
3619
3964
|
?is-saving=${this.isSaving}
|
|
3620
3965
|
?search-disabled=${this.getRevisionsWindow()?.isViewingRevision ?? false}
|
|
3966
|
+
.languageOptions=${languageOptions}
|
|
3967
|
+
current-language-name=${currentLanguage.name}
|
|
3968
|
+
?is-base-language=${isBaseSelected}
|
|
3969
|
+
.languagePercent=${percent}
|
|
3970
|
+
?show-localization-tools=${Boolean(activeLanguage)}
|
|
3621
3971
|
@temba-button-clicked=${this.handleToolbarAction}
|
|
3622
3972
|
></temba-editor-toolbar>
|
|
3623
3973
|
`;
|
|
@@ -3653,6 +4003,13 @@ export class Editor extends RapidElement {
|
|
|
3653
4003
|
case 'search':
|
|
3654
4004
|
this.openFlowSearch();
|
|
3655
4005
|
break;
|
|
4006
|
+
case 'language-change':
|
|
4007
|
+
if (detail.isPrimary) {
|
|
4008
|
+
this.handleLanguageChange(this.definition?.language || '');
|
|
4009
|
+
} else if (detail.languageCode) {
|
|
4010
|
+
this.handleLanguageChange(detail.languageCode);
|
|
4011
|
+
}
|
|
4012
|
+
break;
|
|
3656
4013
|
}
|
|
3657
4014
|
}
|
|
3658
4015
|
|
|
@@ -3997,7 +4354,7 @@ export class Editor extends RapidElement {
|
|
|
3997
4354
|
: ''}
|
|
3998
4355
|
<temba-flow-search
|
|
3999
4356
|
.scope=${this.showMessageTable ? 'table' : 'flow'}
|
|
4000
|
-
.includeCategories=${
|
|
4357
|
+
.includeCategories=${this.isTranslating && this.hasAnyNodeWithLocalizeCategories()}
|
|
4001
4358
|
@temba-search-result-selected=${this.handleSearchResultSelected}
|
|
4002
4359
|
></temba-flow-search>
|
|
4003
4360
|
${!this.showMessageTable && this.flowIssues?.length
|