Skip to content

Commit

Permalink
feat: support dynamic language switching (#359)
Browse files Browse the repository at this point in the history
  • Loading branch information
kswenson authored Oct 21, 2024
1 parent d05967b commit 77a6ad9
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 39 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"name": "@concord-consortium/cloud-file-manager",
"description": "Wrapper for providing file management for web applications",
"author": "The Concord Consortium",
"version": "2.0.0-pre.4",
"version": "2.0.0-pre.6",
"repository": {
"type": "git",
"url": "https://github.com/concord-consortium/cloud-file-manager"
"url": "git+https://github.com/concord-consortium/cloud-file-manager.git"
},
"engines": {
"node": ">= 16",
Expand Down Expand Up @@ -107,7 +107,7 @@
"publish:npm:next": "npm publish --access public --tag next",
"publish:npm:next:preview": "npm publish --access public --tag next --dry-run",
"publish:yalc": "npx yalc publish",
"strings:build": "./node_modules/.bin/strip-json-comments src/code/utils/lang/en-US-master.json > src/code/utils/lang/en-US.json",
"strings:build": "strip-json-comments src/code/utils/lang/en-US-master.json > src/code/utils/lang/en-US.json",
"strings:pull:usage": "echo Usage: `npm run strings:pull -- -a <poeditor_api_key>`",
"strings:pull": "./bin/strings-pull-project.sh",
"strings:push:usage": "echo Usage: `npm run strings:push -- -a <poeditor_api_key>`",
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ export interface CFMMenuBarOptions {
languageMenu?: {
currentLang: string
options: { label: string, langCode: string }[]
onLangChanged?: (langCode: string) => void
}
onLangChanged?: (langCode: string) => void
}

export interface CFMShareDialogSettings {
Expand Down
9 changes: 6 additions & 3 deletions src/code/app-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,24 @@ export interface CFMMenuBarOptions {
languageMenu?: {
currentLang: string
options: { label: string, langCode: string }[]
onLangChanged?: (langCode: string) => void
}
onLangChanged?: (langCode: string) => void
}

export interface CFMShareDialogSettings {
serverUrl?: string
serverUrlLabel?: string
}

export interface CFMUIOptions {
menuBar?: CFMMenuBarOptions
export interface CFMUIMenuOptions {
// null => no menu; undefined => default menu
menu?: CFMMenu | null
// map from menu item string to menu display name for string-only menu items
menuNames?: Record<string, string>
}

export interface CFMUIOptions extends CFMUIMenuOptions {
menuBar?: CFMMenuBarOptions
// used for setting the page title from the document name (see appSetsWindowTitle)
windowTitleSuffix?: string
windowTitleSeparator?: string
Expand Down
9 changes: 7 additions & 2 deletions src/code/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import $ from 'jquery'
import _ from 'lodash'
import mime from 'mime'

import tr from './utils/translate'
import tr, { setCurrentLanguage } from './utils/translate'
import isString from './utils/is-string'
import base64Array from 'base64-js' // https://github.com/beatgammit/base64-js
import getQueryParam from './utils/get-query-param'

import { CFMAppOptions, CFMMenuItem, isCustomClientProvider } from './app-options'
import { CFMAppOptions, CFMMenuItem, CFMUIMenuOptions, isCustomClientProvider } from './app-options'
import { CloudFileManagerUI, UIEventCallback } from './ui'

import LocalStorageProvider from './providers/localstorage-provider'
Expand Down Expand Up @@ -349,6 +349,10 @@ class CloudFileManagerClient {
}
}

replaceMenu(options: CFMUIMenuOptions) {
this._ui.replaceMenu(options)
}

appendMenuItem(item: CFMMenuItem) {
this._ui.appendMenuItem(item)
return this
Expand Down Expand Up @@ -1162,6 +1166,7 @@ class CloudFileManagerClient {
}

changeLanguage(newLangCode: string, callback: (newLangCode?: string) => void) {
setCurrentLanguage(newLangCode)
if (callback) {
const postSave = (err: string | null) => {
if (err) {
Expand Down
13 changes: 9 additions & 4 deletions src/code/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import { CloudFileManagerClient } from './client'
import { CFMMenu, CFMMenuItem, CFMUIOptions } from './app-options'
import { CFMMenu, CFMMenuItem, CFMUIMenuOptions, CFMUIOptions } from './app-options'
import tr from './utils/translate'
import isString from './utils/is-string'
import { SelectInteractiveStateDialogProps } from './views/select-interactive-state-dialog-view'
Expand All @@ -29,9 +29,9 @@ class CloudFileManagerUIMenu {
static DefaultMenu: CFMMenu = ['newFileDialog', 'openFileDialog', 'revertSubMenu', 'separator', 'save', 'createCopy', 'shareSubMenu', 'renameDialog']

items: CFMMenuItem[]
options: CFMUIOptions
options: CFMUIMenuOptions

constructor(options: CFMUIOptions, client: CloudFileManagerClient) {
constructor(options: CFMUIMenuOptions, client: CloudFileManagerClient) {
this.options = options
this.items = this.parseMenuItems(options.menu, client)
}
Expand Down Expand Up @@ -97,7 +97,7 @@ class CloudFileManagerUIMenu {
const item = menuItems[i]
if (item === 'separator') {
menuItem = {
key: `seperator${i}`,
key: `separator${i}`,
separator: true
}
} else if (isString(item)) {
Expand Down Expand Up @@ -168,6 +168,11 @@ class CloudFileManagerUI {
})
}

replaceMenu(options: CFMUIMenuOptions) {
this.menu = new CloudFileManagerUIMenu(options, this.client)
this.listenerCallback(new CloudFileManagerUIEvent('replaceMenu', options))
}

appendMenuItem(item: CFMMenuItem) {
return this.listenerCallback(new CloudFileManagerUIEvent('appendMenuItem', item))
}
Expand Down
52 changes: 31 additions & 21 deletions src/code/utils/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,22 @@ interface LanguageFileEntry {
}

const languageFiles: LanguageFileEntry[] = [
{key: 'de', contents: de}, // German
{key: 'el', contents: el}, // Greek
{key: 'en-US', contents: enUS}, // US English
{key: 'es', contents: es}, // Spanish
{key: 'fa', contents: fa}, // Farsi (Persian)
{key: 'he', contents: he}, // Hebrew
{key: 'ja' , contents: ja}, // Japanese
{key: 'ko' , contents: ko}, // Korean
{key: 'nb', contents: nb}, // Norwegian Bokmål
{key: 'nn', contents: nn}, // Norwegian Nynorsk
{key: 'pl', contents: pl}, // Polish Polski
{key: 'pt-BR', contents: ptBR}, // Brazilian Portuguese
{key: 'th', contents: th}, // Thai
{key: 'tr', contents: tr}, // Turkish
{key: 'zh', contents: zhHans}, // Simplified Chinese
{key: 'zh-TW', contents: zhTW} // Traditional Chinese (Taiwan)
{key: 'de', contents: de}, // German
{key: 'el', contents: el}, // Greek
{key: 'en-US', contents: enUS}, // US English
{key: 'es', contents: es}, // Spanish
{key: 'fa', contents: fa}, // Farsi (Persian)
{key: 'he', contents: he}, // Hebrew
{key: 'ja' , contents: ja}, // Japanese
{key: 'ko' , contents: ko}, // Korean
{key: 'nb', contents: nb}, // Norwegian Bokmål
{key: 'nn', contents: nn}, // Norwegian Nynorsk
{key: 'pl', contents: pl}, // Polish Polski
{key: 'pt-BR', contents: ptBR}, // Brazilian Portuguese
{key: 'th', contents: th}, // Thai
{key: 'tr', contents: tr}, // Turkish
{key: 'zh-Hans', contents: zhHans}, // Simplified Chinese
{key: 'zh-TW', contents: zhTW} // Traditional Chinese (Taiwan)
]

// returns baseLANG from baseLANG-REGION if REGION exists
Expand Down Expand Up @@ -84,23 +84,33 @@ languageFiles.forEach(function(lang) {

const lang = (urlParams as any).lang || getPageLanguage() || getFirstBrowserLanguage()
const baseLang = getBaseLanguage(lang || '')
// CODAP/Sproutcore lower cases language in documentElement
const defaultLang = lang && translations[lang.toLowerCase()] ? lang : baseLang && translations[baseLang] ? baseLang : "en"
// CODAP/SproutCore lower cases language in documentElement
const defaultLang: string = lang && translations[lang.toLowerCase()] ? lang : baseLang && translations[baseLang] ? baseLang : "en"

let gCurrentLanguage = defaultLang

export function getCurrentLanguage() {
return gCurrentLanguage
}

export function setCurrentLanguage(lang: string) {
gCurrentLanguage = lang
}

// console.log(`CFM: using ${defaultLang} for translation (lang is "${(urlParams as any).lang}" || "${getFirstBrowserLanguage()}")`)

const varRegExp = /%\{\s*([^}\s]*)\s*\}/g

const translate = function(key: string, vars?: Record<string ,string>, lang?: string) {
const translate = function(key: string, vars?: Record<string, string>, lang?: string) {
if (vars == null) { vars = {} }
if (lang == null) { lang = defaultLang }
if (lang == null) { lang = gCurrentLanguage }
lang = lang.toLowerCase()
let translation = translations[lang] != null ? translations[lang][key] : undefined
if ((translation == null)) { translation = key }
return translation.replace(varRegExp, function(match: string, key: string) {
return Object.prototype.hasOwnProperty.call(vars, key)
? vars[key]
: `'** UKNOWN KEY: ${key} **`
: `'** UNKNOWN KEY: ${key} **`
})
}

Expand Down
2 changes: 2 additions & 0 deletions src/code/views/app-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ class AppView extends React.Component<IAppViewProps, IAppViewState> {
return this.setState({confirmDialog: event.data})
case 'showSelectInteractiveStateDialog':
return this.setState({selectInteractiveStateDialog: event.data})
case 'replaceMenu':
return this.setState({ menuItems: this.props.client._ui.menu.items })
case 'appendMenuItem':
this.state.menuItems.push(event.data)
return this.setState({menuItems: this.state.menuItems})
Expand Down
7 changes: 4 additions & 3 deletions src/code/views/menu-bar-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ReactDOMFactories from "react-dom-factories"
import { createReactFactory } from '../create-react-factory'
import DropDownView from "./dropdown-view"
import {TriangleOnlyAnchor} from './dropdown-anchors'
import tr from '../utils/translate'
import tr, { getCurrentLanguage } from '../utils/translate'

const {div, i, span, input} = ReactDOMFactories
const Dropdown = createReactFactory(DropDownView)
Expand Down Expand Up @@ -158,9 +158,10 @@ export default createReactClass({

renderLanguageMenu() {
const langMenu = this.props.options.languageMenu
const currentLang = getCurrentLanguage()
const items = langMenu.options
// Do not show current language in the menu.
.filter((option: any) => option.langCode !== langMenu.currentLang)
.filter((option: any) => currentLang !== option.langCode)
.map((option: any) => {
let className
const label = option.label || option.langCode.toUpperCase()
Expand All @@ -172,7 +173,7 @@ export default createReactClass({
})

const hasFlags = langMenu.options.filter((option: any) => option.flag != null).length > 0
const currentOption = langMenu.options.filter((option: any) => option.langCode === langMenu.currentLang)[0]
const currentOption = langMenu.options.filter((option: any) => currentLang === option.langCode)[0]
const defaultOption = hasFlags ? {flag: "us"} : {label: "English"}
const {flag, label} = currentOption || defaultOption
const menuAnchor = flag ?
Expand Down

0 comments on commit 77a6ad9

Please sign in to comment.