diff --git a/.circleci/config.yml b/.circleci/config.yml index 3f59d9481..4529184df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,6 +46,10 @@ jobs: name: Lint command: yarn run lint + - run: + name: Locale + command: yarn run locale:verify + - run: name: Unit tests command: | diff --git a/apis/nucleus/src/components/LongRunningQuery.jsx b/apis/nucleus/src/components/LongRunningQuery.jsx index 5e2da3090..d4062ddd0 100644 --- a/apis/nucleus/src/components/LongRunningQuery.jsx +++ b/apis/nucleus/src/components/LongRunningQuery.jsx @@ -1,7 +1,8 @@ /* eslint-disable react/jsx-props-no-spreading */ -import React, { useState } from 'react'; +import React, { useState, useContext } from 'react'; import { makeStyles, Grid, Typography, Button } from '@material-ui/core'; import WarningTriangle from '@nebula.js/ui/icons/warning-triangle-2'; +import LocaleContext from '../contexts/LocaleContext'; import Progress from './Progress'; @@ -22,7 +23,7 @@ const useStyles = makeStyles(() => ({ }, })); -const Cancel = ({ cancel, ...props }) => ( +const Cancel = ({ cancel, translator, ...props }) => ( <> @@ -30,19 +31,19 @@ const Cancel = ({ cancel, ...props }) => ( - Updating data + {translator.get('Object.Update.Active')} ); -const Retry = ({ retry, ...props }) => ( +const Retry = ({ retry, translator, ...props }) => ( <> @@ -50,11 +51,12 @@ const Retry = ({ retry, ...props }) => ( Data update was cancelled + {translator.get('Object.Update.Cancelled')} @@ -64,6 +66,8 @@ export default function LongRunningQuery({ onCancel, onRetry }) { const { stripes, cancel, retry } = useStyles(); const [canCancel, setCanCancel] = useState(!!onCancel); const [canRetry, setCanRetry] = useState(!!onRetry); + const translator = useContext(LocaleContext); + const handleCancel = () => { setCanCancel(false); setCanRetry(true); @@ -90,8 +94,8 @@ export default function LongRunningQuery({ onCancel, onRetry }) { }} spacing={2} > - {canCancel && } - {canRetry && } + {canCancel && } + {canRetry && } ); } diff --git a/apis/nucleus/src/components/selections/OneField.jsx b/apis/nucleus/src/components/selections/OneField.jsx index e120d6db9..d6894ffd6 100644 --- a/apis/nucleus/src/components/selections/OneField.jsx +++ b/apis/nucleus/src/components/selections/OneField.jsx @@ -61,8 +61,6 @@ export default function OneField({ field, api, stateIx = 0, skipHandleShowListBo label = translator.get('CurrentSelections.All'); } else if (numSelected > 1 && selection.qTotal) { label = translator.get('CurrentSelections.Of', [numSelected, selection.qTotal]); - } else if (noSegments) { - label = translator.get('CurrentSelections.None'); } else { label = selection.qSelectedFieldSelectionInfo.map(v => v.qName).join(', '); } diff --git a/apis/nucleus/src/components/selections/__tests__/one-field.spec.jsx b/apis/nucleus/src/components/selections/__tests__/one-field.spec.jsx index cec2a3f46..71b8cdb34 100644 --- a/apis/nucleus/src/components/selections/__tests__/one-field.spec.jsx +++ b/apis/nucleus/src/components/selections/__tests__/one-field.spec.jsx @@ -91,6 +91,7 @@ describe('', () => { selections: [ { qField: 'my-field', + qSelectedFieldSelectionInfo: [], }, ], states: ['$'], @@ -111,6 +112,7 @@ describe('', () => { { qField: 'my-field', qLocked: true, + qSelectedFieldSelectionInfo: [], }, ], states: ['$'], diff --git a/apis/nucleus/src/locale/app-locale.js b/apis/nucleus/src/locale/app-locale.js index 2cba7be85..4915ecc4e 100644 --- a/apis/nucleus/src/locale/app-locale.js +++ b/apis/nucleus/src/locale/app-locale.js @@ -1,18 +1,13 @@ import localeFn from '@nebula.js/locale'; -import en from './translations/en-US'; +import all from './translations/all.json'; export default function appLocaleFn({ language }) { const l = localeFn({ initial: language, }); - Object.keys(en).forEach(id => { - l.translator.add({ - id, - locale: { - 'en-US': en[id], - }, - }); + Object.keys(all).forEach(key => { + l.translator.add(all[key]); }); return { diff --git a/apis/nucleus/src/locale/translations/all.json b/apis/nucleus/src/locale/translations/all.json new file mode 100644 index 000000000..6d063fbe9 --- /dev/null +++ b/apis/nucleus/src/locale/translations/all.json @@ -0,0 +1,394 @@ +{ + "Object_Update_Active": { + "id": "Object.Update.Active", + "locale": { + "en-US": "Updating data" + } + }, + "Object_Update_Cancelled": { + "id": "Object.Update.Cancelled", + "locale": { + "en-US": "Data update was cancelled" + } + }, + "Supernova_Incomplete": { + "id": "Supernova.Incomplete", + "locale": { + "en-US": "Incomplete visualization", + "it-IT": "Visualizzazione incompleta", + "zh-CN": "不完整的可视化", + "zh-TW": "視覺化未完成", + "ko-KR": "완료되지 않은 시각화", + "de-DE": "Unvollständige Visualisierung", + "sv-SE": "Ofullständig visualisering", + "es-ES": "Visualización incompleta", + "pt-BR": "Visualização incompleta", + "ja-JP": "未完了のビジュアライゼーション", + "fr-FR": "Visualisation incomplète", + "nl-NL": "Onvolledige visualisatie", + "tr-TR": "Tamamlanmamış görselleştirme", + "pl-PL": "Niekompletna wizualizacja", + "ru-RU": "Незавершенная визуализация" + } + }, + "Cancel": { + "id": "Common.Cancel", + "locale": { + "en-US": "Cancel", + "de-DE": "Abbrechen", + "es-ES": "Cancelar", + "fr-FR": "Annuler", + "ja-JP": "キャンセル", + "nl-NL": "Annuleren", + "it-IT": "Annulla", + "ko-KR": "취소", + "pl-PL": "Anuluj", + "ru-RU": "Отмена", + "pt-BR": "Cancelar", + "sv-SE": "Avbryt", + "zh-CN": "取消", + "tr-TR": "İptal", + "zh-TW": "取消" + } + }, + "OK": { + "id": "Common.OK", + "locale": { + "en-US": "OK", + "de-DE": "OK", + "es-ES": "Aceptar", + "fr-FR": "OK", + "ja-JP": "OK", + "nl-NL": "OK", + "it-IT": "OK", + "ko-KR": "확인", + "pl-PL": "OK", + "ru-RU": "ОК", + "pt-BR": "OK", + "sv-SE": "OK", + "zh-CN": "确定", + "tr-TR": "Tamam", + "zh-TW": "確定" + } + }, + "Retry": { + "id": "Common.Retry", + "locale": { + "en-US": "Retry", + "it-IT": "Riprova", + "zh-CN": "重试", + "zh-TW": "重試", + "ko-KR": "다시 시도", + "de-DE": "Wiederholen", + "sv-SE": "Försök igen", + "es-ES": "Intentar de nuevo", + "pt-BR": "Tentar Novamente", + "ja-JP": "再試行", + "fr-FR": "Réessayer", + "nl-NL": "Opnieuw", + "tr-TR": "Yeniden dene", + "pl-PL": "Ponów próbę", + "ru-RU": "Повторить попытку" + } + }, + "CurrentSelections_All": { + "id": "CurrentSelections.All", + "locale": { + "en-US": "ALL", + "it-IT": "TUTTO", + "zh-CN": "全部", + "zh-TW": "全部", + "ko-KR": "모두", + "de-DE": "ALLES", + "sv-SE": "ALLA", + "es-ES": "TODOS", + "pt-BR": "TODOS", + "ja-JP": "すべて", + "fr-FR": "TOUS", + "nl-NL": "ALLE", + "tr-TR": "TÜMÜ", + "pl-PL": "WSZYSTKO", + "ru-RU": "ВСЕ" + } + }, + "CurrentSelections_Of": { + "id": "CurrentSelections.Of", + "locale": { + "en-US": "{0} of {1}", + "it-IT": "{0} di {1}", + "zh-CN": "{0}/{1}", + "zh-TW": "{0} / {1}", + "ko-KR": "{0} / {1}", + "de-DE": "{0} von {1}", + "sv-SE": "{0} av {1}", + "es-ES": "{0} de {1}", + "pt-BR": "{0} de {1}", + "ja-JP": "{0} / {1}", + "fr-FR": "{0} sur {1}", + "nl-NL": "{0} van {1}", + "tr-TR": "{0} / {1}", + "pl-PL": "{0} z {1}", + "ru-RU": "{0} из {1}" + } + }, + "Listbox_Search": { + "id": "Listbox.Search", + "locale": { + "en-US": "Search in listbox", + "it-IT": "Cerca nella casella di elenco", + "zh-CN": "在列表框中搜索", + "zh-TW": "在清單方塊中搜尋", + "ko-KR": "목록 상자에서 검색", + "de-DE": "In Listenfeld suchen", + "sv-SE": "Sök i listruta", + "es-ES": "Buscar en cuadro de lista", + "pt-BR": "Pesquisar na caixa de listagem", + "ja-JP": "リストボックス内を検索", + "fr-FR": "Rechercher dans la liste de sélection", + "nl-NL": "Zoeken in keuzelijst", + "tr-TR": "Liste kutusunda ara", + "pl-PL": "Wyszukaj w liście wartości", + "ru-RU": "Поиск в списке" + } + }, + "Navigate_Forward": { + "id": "Navigate.Forward", + "locale": { + "en-US": "Step forward", + "it-IT": "Vai avanti", + "zh-CN": "前进", + "zh-TW": "前進", + "ko-KR": "다음 단계", + "de-DE": "Schritt vor", + "sv-SE": "Gå framåt", + "es-ES": "Avanzar", + "pt-BR": "Avançar uma etapa", + "ja-JP": "1段階進む", + "fr-FR": "Étape suivante", + "nl-NL": "Stap vooruit", + "tr-TR": "Bir adım ileri", + "pl-PL": "Krok do przodu", + "ru-RU": "Шаг вперед" + } + }, + "Navigate_Back": { + "id": "Navigate.Back", + "locale": { + "en-US": "Step back", + "it-IT": "Torna indietro", + "zh-CN": "后退", + "zh-TW": "倒退", + "ko-KR": "이전 단계", + "de-DE": "Schritt zurück", + "sv-SE": "Gå bakåt", + "es-ES": "Retroceder", + "pt-BR": "Voltar uma etapa", + "ja-JP": "1 段階戻る", + "fr-FR": "Retour en arrière", + "nl-NL": "Stap terug", + "tr-TR": "Bir adım geri", + "pl-PL": "Krok do tyłu", + "ru-RU": "Шаг назад" + } + }, + "Selection_ClearAll": { + "id": "Selection.ClearAll", + "locale": { + "en-US": "Clear all selections", + "it-IT": "Cancella tutte le selezioni", + "zh-CN": "清除所有选择项", + "zh-TW": "清除所有選項", + "ko-KR": "모든 선택 해제", + "de-DE": "Auswahl aufheben (alle Felder)", + "sv-SE": "Rensa alla urval", + "es-ES": "Borrar todas las selecciones", + "pt-BR": "Limpar todas as seleções", + "ja-JP": "選択をすべてクリアする", + "fr-FR": "Effacer toutes les sélections", + "nl-NL": "Alle selecties wissen", + "tr-TR": "Tüm seçimleri temizle", + "pl-PL": "Wyczyść wszystkie selekcje", + "ru-RU": "Очистить от всех выборок" + } + }, + "Selection_ClearAllStates": { + "id": "Selection.ClearAllStates", + "locale": { + "en-US": "Clear all states", + "it-IT": "Cancella tutti gli stati", + "zh-CN": "清除所有状态", + "zh-TW": "清除所有狀態", + "ko-KR": "모든 상태 지우기", + "de-DE": "Alle Status löschen", + "sv-SE": "Rensa alla tillstånd", + "es-ES": "Borrar todos los estados", + "pt-BR": "Limpar todos os estados", + "ja-JP": "全ステートをクリア", + "fr-FR": "Effacer tous les états", + "nl-NL": "Alle states wissen", + "tr-TR": "Tüm durumları temizle", + "pl-PL": "Wyczyść wszystkie stany", + "ru-RU": "Очистить все состояния" + } + }, + "Selection_Confirm": { + "id": "Selection.Confirm", + "locale": { + "en-US": "Confirm selection", + "it-IT": "Conferma selezione", + "zh-CN": "确认选择", + "zh-TW": "確認選取", + "ko-KR": "선택 확인", + "de-DE": "Auswahl bestätigen", + "sv-SE": "Bekräfta urval", + "es-ES": "Confirmar selección", + "pt-BR": "Confirmar seleção", + "ja-JP": "選択の確認", + "fr-FR": "Confirmer la sélection", + "nl-NL": "Selectie bevestigen", + "tr-TR": "Seçimi onayla", + "pl-PL": "Potwierdź selekcję", + "ru-RU": "Подтвердить выборку" + } + }, + "Selection_Cancel": { + "id": "Selection.Cancel", + "locale": { + "en-US": "Cancel selection", + "it-IT": "Annulla selezione", + "zh-CN": "取消选择", + "zh-TW": "取消選取", + "ko-KR": "선택 취소", + "de-DE": "Auswahl abbrechen", + "sv-SE": "Avbryt urval", + "es-ES": "Cancelar selección", + "pt-BR": "Cancelar seleção", + "ja-JP": "選択のキャンセル", + "fr-FR": "Annuler la sélection", + "nl-NL": "Selectie annuleren", + "tr-TR": "Seçimi iptal et", + "pl-PL": "Anuluj selekcję", + "ru-RU": "Отменить выборку" + } + }, + "Selection_Clear": { + "id": "Selection.Clear", + "locale": { + "en-US": "Clear selection", + "it-IT": "Cancella selezione", + "zh-CN": "清除选择", + "zh-TW": "清除選項", + "ko-KR": "선택 해제", + "de-DE": "Auswahl löschen", + "sv-SE": "Rensa urval", + "es-ES": "Borrar selección", + "pt-BR": "Limpar seleção", + "ja-JP": "選択をクリア", + "fr-FR": "Effacer la sélection", + "nl-NL": "Selectie wissen", + "tr-TR": "Seçimi temizle", + "pl-PL": "Wyczyść selekcję", + "ru-RU": "Очистить выбор" + } + }, + "Selection_SelectAll": { + "id": "Selection.SelectAll", + "locale": { + "en-US": "Select all", + "it-IT": "Seleziona tutto", + "zh-CN": "全选", + "zh-TW": "全選", + "ko-KR": "모두 선택", + "de-DE": "Alle Werte auswählen", + "sv-SE": "Markera alla", + "es-ES": "Seleccionar todo", + "pt-BR": "Selecionar todos", + "ja-JP": "すべて選択", + "fr-FR": "Sélectionner tout", + "nl-NL": "Alles selecteren", + "tr-TR": "Tümünü seç", + "pl-PL": "Wybierz wszystko", + "ru-RU": "Выбрать все" + } + }, + "Selection_SelectAlternative": { + "id": "Selection.SelectAlternative", + "locale": { + "en-US": "Select alternative", + "it-IT": "Seleziona alternativi", + "zh-CN": "选择替代项", + "zh-TW": "選取替代選項", + "ko-KR": "대안 선택", + "de-DE": "Alternative Werte auswählen", + "sv-SE": "Välj alternativ", + "es-ES": "Seleccionar alternativos", + "pt-BR": "Selecionar alternativa", + "ja-JP": "代替値を選択", + "fr-FR": "Sélectionner des valeurs alternatives", + "nl-NL": "Alternatief selecteren", + "tr-TR": "Alternatifi seç", + "pl-PL": "Wybierz alternatywę", + "ru-RU": "Выбрать альтернативные" + } + }, + "Selection_SelectExcluded": { + "id": "Selection.SelectExcluded", + "locale": { + "en-US": "Select excluded", + "it-IT": "Seleziona esclusi", + "zh-CN": "选择排除项", + "zh-TW": "選取排除值", + "ko-KR": "제외 항목 선택", + "de-DE": "Ausgeschlossene Werte auswählen", + "sv-SE": "Välj uteslutna", + "es-ES": "Seleccionar excluidos", + "pt-BR": "Selecionar excluído", + "ja-JP": "除外値を選択", + "fr-FR": "Sélectionner les valeurs exclues", + "nl-NL": "Uitgesloten selecteren", + "tr-TR": "Hariç tutulanı seç", + "pl-PL": "Wybierz wykluczone", + "ru-RU": "Выбрать исключенные" + } + }, + "Selection_SelectPossible": { + "id": "Selection.SelectPossible", + "locale": { + "en-US": "Select possible", + "it-IT": "Seleziona possibili", + "zh-CN": "选择可能值", + "zh-TW": "選取可能值", + "ko-KR": "사용 가능 항목 선택", + "de-DE": "Wählbare Werte auswählen", + "sv-SE": "Välj möjliga", + "es-ES": "Seleccionar posibles", + "pt-BR": "Selecionar possível", + "ja-JP": "絞込値を選択", + "fr-FR": "Sélectionner les valeurs possibles", + "nl-NL": "Mogelijke selecteren", + "tr-TR": "Olasıyı seç", + "pl-PL": "Wybierz możliwe", + "ru-RU": "Выбрать возможные" + } + }, + "Selection_Menu": { + "id": "Selection.Menu", + "locale": { + "en-US": "Selection menu", + "it-IT": "Menu Selezione", + "zh-CN": "选择菜单", + "zh-TW": "選項功能表", + "ko-KR": "선택 메뉴", + "de-DE": "Auswahlmenü", + "sv-SE": "Urvalsmeny", + "es-ES": "Menú de selección", + "pt-BR": "Menu de seleção", + "ja-JP": "選択メニュー", + "fr-FR": "Menu Sélection", + "nl-NL": "Selectiemenu", + "tr-TR": "Seçim menüsü", + "pl-PL": "Menu selekcji", + "ru-RU": "Меню \"Выборка\"" + } + } +} diff --git a/apis/nucleus/src/locale/translations/en-US.js b/apis/nucleus/src/locale/translations/en-US.js deleted file mode 100644 index c7817adf2..000000000 --- a/apis/nucleus/src/locale/translations/en-US.js +++ /dev/null @@ -1,22 +0,0 @@ -export default { - 'Common.Cancel': 'Cancel', - 'Common.OK': 'Ok', - 'Common.English': 'English', - 'CurrentSelections.All': 'ALL', - 'CurrentSelections.Of': '{0} of {1}', - 'CurrentSelections.None': 'NONE', - 'Listbox.Search': 'Search in listbox', - 'Navigate.Forward': 'Step forward', - 'Navigate.Back': 'Step back', - 'Selection.ClearAll': 'Clear all selections', - 'Selection.ClearAllStates': 'Clear all states', - 'Selection.Confirm': 'Confirm selection', - 'Selection.Cancel': 'Cancel selection', - 'Selection.Clear': 'Clear selection', - 'Selection.SelectAll': 'Select all', - 'Selection.SelectAlternative': 'Select alternative', - 'Selection.SelectExcluded': 'Select excluded', - 'Selection.SelectPossible': 'Select possible', - 'Selection.Menu': 'Selection menu', - 'Supernova.Incomplete': 'Incomplete visualization', -}; diff --git a/package.json b/package.json index dfae388ce..73fa22f8b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "build": "cross-env NODE_ENV=production FORCE_COLOR=1 lerna run build --stream", "build:codesandbox": "cross-env NODE_ENV=production CODESANDBOX=1 FORCE_COLOR=1 lerna run build --stream --scope \"@nebula.js/{nucleus,supernova,theme}\"", "build:watch": "FORCE_COLOR=1 lerna run build:watch --stream --concurrency 99 --no-sort", + "locale:verify": "node tools/verify-translations.js", "lint": "eslint packages apis commands --ext .js,.jsx", "lint:check": "eslint --print-config ./aw.config.js | eslint-config-prettier-check", "start": "MONO=true ./commands/cli/lib/index.js serve --entry ./test/integration/sn.js", @@ -33,6 +34,7 @@ "@after-work.js/aw": "6.0.10", "@babel/cli": "7.7.4", "@babel/core": "7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-transform-react-jsx": "7.7.4", "@babel/preset-env": "7.7.4", "@babel/preset-react": "7.7.4", diff --git a/rollup.config.js b/rollup.config.js index c9080aaa0..47c416803 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,6 +6,8 @@ const replace = require('rollup-plugin-replace'); const json = require('rollup-plugin-json'); const { terser } = require('rollup-plugin-terser'); +const localeStringValidator = require('./tools/locale-string-validator'); + const cwd = process.cwd(); const pkg = require(path.join(cwd, 'package.json')); // eslint-disable-line const { name, version, license } = pkg; @@ -150,7 +152,7 @@ const config = isEsm => { }, ], ], - plugins: [['@babel/plugin-transform-react-jsx']], + plugins: [['@babel/plugin-transform-react-jsx'], [localeStringValidator, {}]], }), ], }; diff --git a/tools/locale-string-validator.js b/tools/locale-string-validator.js new file mode 100644 index 000000000..c459ce691 --- /dev/null +++ b/tools/locale-string-validator.js @@ -0,0 +1,56 @@ +const { declare } = require('@babel/helper-plugin-utils'); + +const vars = require('../apis/nucleus/src/locale/translations/all.json'); + +const ids = {}; +Object.keys(vars).forEach(key => { + ids[vars[key].id] = key; +}); + +const used = []; +const warnings = {}; + +const warn = s => console.warn(`\x1b[43m\x1b[30m WARN \x1b[0m \x1b[1m\x1b[33m ${s}\x1b[0m`); + +const find = declare((/* api, options */) => { + function useString(id) { + if (used.indexOf(id) !== -1) { + return; + } + + used.push(id); + + if (typeof ids[id] === 'undefined') { + warn(`String '${id}' does not exist in locale registry`); + } + } + return { + name: 'find', + visitor: { + CallExpression(path) { + if (!path.get('callee').isMemberExpression()) { + return; + } + if ( + path.node.callee.object && + path.node.callee.object.name === 'translator' && + path.node.callee.property && + path.node.callee.property.name === 'get' + ) { + const { type, value } = path.node.arguments[0]; + if (type === 'StringLiteral') { + useString(value, path); + } else { + const s = `${this.file.opts.filename}:${path.node.loc.start.line}:${path.node.loc.start.column}`; + if (!warnings[s]) { + warnings[s] = true; + warn(`Could not verify used string at ${s}`); + } + } + } + }, + }, + }; +}); + +module.exports = find; diff --git a/tools/verify-translations.js b/tools/verify-translations.js new file mode 100644 index 000000000..66db0f98c --- /dev/null +++ b/tools/verify-translations.js @@ -0,0 +1,32 @@ +const vars = require('../apis/nucleus/src/locale/translations/all.json'); + +const languages = [ + 'en-US', + 'it-IT', + 'zh-CN', + 'zh-TW', + 'ko-KR', + 'de-DE', + 'sv-SE', + 'es-ES', + 'pt-BR', + 'ja-JP', + 'fr-FR', + 'nl-NL', + 'tr-TR', + 'pl-PL', + 'ru-RU', +]; + +Object.keys(vars).forEach(key => { + const supportLanguagesForString = Object.keys(vars[key].locale); + if (supportLanguagesForString.indexOf('en-US') === -1) { + // en-US must exist + throw new Error(`String '${vars[key].id}' is missing value for 'en-US'`); + } + for (let i = 0; i < languages.length; i++) { + if (supportLanguagesForString.indexOf(languages[i]) === -1) { + console.warn(`String '${vars[key].id}' is missing value for '${languages[i]}'`); + } + } +});