Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3e3b705
build(common): add junit test dependency for upcoming i18n tests
JackieTien97 May 18, 2026
00d7902
feat(i18n): add Messages utility for runtime locale resolution
JackieTien97 May 18, 2026
4219cd5
test(i18n): add drift-prevention tests for messages bundle alignment
JackieTien97 May 18, 2026
d0d4da3
refactor(common): migrate log and exception messages to i18n keys
JackieTien97 May 18, 2026
628fe28
refactor(write): migrate log and exception messages to i18n keys
JackieTien97 May 18, 2026
a8cd670
refactor(read.common): migrate messages to i18n keys
JackieTien97 May 18, 2026
74d3a2b
refactor(read): migrate remaining read-path messages to i18n keys
JackieTien97 May 18, 2026
6baec28
refactor(encoding): migrate messages to i18n keys
JackieTien97 May 18, 2026
236b070
refactor(file): migrate messages to i18n keys
JackieTien97 May 18, 2026
78c1643
refactor(external): migrate messages to i18n keys
JackieTien97 May 18, 2026
bd0b26a
refactor(tsfile): migrate encrypt/compress/fileSystem/utils messages …
JackieTien97 May 18, 2026
adf9905
refactor(tools): migrate log and exception messages to i18n keys
JackieTien97 May 18, 2026
eb938f7
fixup(encrypt): use Messages.get for SHA-256 not-found exception
JackieTien97 May 18, 2026
76811fd
i18n: translate tsfile message bundle to Simplified Chinese
JackieTien97 May 18, 2026
7c880c3
docs: document i18n convention for tsfile Java code
JackieTien97 May 18, 2026
a95c96f
fix(i18n): isolate locale resolution and pin tests to English bundle
JackieTien97 May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ The root `pom.xml` orchestrates all three via Maven profiles:
./mvnw spotless:check # Check formatting without modifying
```

## Internationalization

Java logs and exception messages use a runtime `ResourceBundle` approach so the same JAR can emit English or Simplified Chinese based on a JVM startup property. See `java/CLAUDE.md` for the convention. Switch language with `-Dtsfile.locale=zh`.

## License Header

Every new file must include the Apache License 2.0 header at the top. Use the comment style appropriate for the file type (e.g., `<!-- -->` for Markdown, `/* */` for Java/C++, `#` for Python). See any existing file for the exact wording.
Expand Down
17 changes: 17 additions & 0 deletions java/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,23 @@ Code generation: FreeMarker templates in `tsfile/src/main/codegen/` generate typ
- Integration tests: `*IT.java` — run via `mvn verify`
- Tests run in random order with `reuseForks=false`

## Internationalization (i18n)

All user-facing log messages and exception messages in `java/common`, `java/tsfile`, and `java/tools` go through `org.apache.tsfile.i18n.Messages`. Hardcoded English strings in `LOGGER.*` / `LOG.*` / `logger.*` or `throw new XxxException(...)` are not allowed for new code — add an entry to `java/common/src/main/resources/org/apache/tsfile/i18n/messages.properties` and reference it via:

- `Messages.get(key)` for SLF4J patterns (containing `{}` placeholders)
- `Messages.format(key, args)` for exception patterns (containing `%1$s` positional placeholders)

Locale at runtime is chosen in this order:

1. `-Dtsfile.locale=<tag>` system property (e.g. `zh`, `zh-CN`)
2. `Locale.getDefault()`
3. English fallback (root bundle)

When adding a key, mirror it in `messages_zh.properties` with a Simplified Chinese translation. `MessagesTest` enforces key-set alignment between the two bundles — CI fails on drift.

Technical terms (class names, method names, type names, codec names like `TsFile`, `Chunk`, `Page`, `Schema`, `RLE`, `GORILLA`) are kept in English in Chinese translations.

## License Header

Every new file must include the Apache License 2.0 header at the top. For Java files, use the `/* */` block comment style. See any existing `.java` file for the exact wording.
Expand Down
23 changes: 23 additions & 0 deletions java/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@
</parent>
<artifactId>common</artifactId>
<name>TsFile: Java: Common</name>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Pin tsfile.locale=en so tests run against the English (root) message
bundle regardless of the JVM default locale. Must be set at JVM
startup because Messages.BUNDLE is initialized once at class load. -->
<systemPropertyVariables>
<tsfile.locale>en</tsfile.locale>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>get-jar-with-dependencies</id>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.apache.tsfile.block;

import org.apache.tsfile.block.column.ColumnBuilderStatus;
import org.apache.tsfile.i18n.Messages;

public class TsBlockBuilderStatus {

Expand Down Expand Up @@ -49,7 +50,7 @@ public boolean isFull() {

public void addBytes(int bytes) {
if (bytes < 0) {
throw new IllegalArgumentException("bytes cannot be negative");
throw new IllegalArgumentException(Messages.get("error.common.bytes_cannot_be_negative"));
}
currentSize += bytes;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
package org.apache.tsfile.block.column;

import org.apache.tsfile.block.TsBlockBuilderStatus;
import org.apache.tsfile.i18n.Messages;
import org.apache.tsfile.utils.RamUsageEstimator;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

public class ColumnBuilderStatus {
Expand Down Expand Up @@ -61,20 +61,22 @@ public String toString() {
private static long deepInstanceSize(Class<?> clazz) {
if (clazz.isArray()) {
throw new IllegalArgumentException(
format(
"Cannot determine size of %s because it contains an array", clazz.getSimpleName()));
Messages.format("error.common.cannot_determine_size_array", clazz.getSimpleName()));
}
if (clazz.isInterface()) {
throw new IllegalArgumentException(format("%s is an interface", clazz.getSimpleName()));
throw new IllegalArgumentException(
Messages.format("error.common.class_is_interface", clazz.getSimpleName()));
}
if (Modifier.isAbstract(clazz.getModifiers())) {
throw new IllegalArgumentException(format("%s is abstract", clazz.getSimpleName()));
throw new IllegalArgumentException(
Messages.format("error.common.class_is_abstract", clazz.getSimpleName()));
}
if (!clazz.getSuperclass().equals(Object.class)) {
throw new IllegalArgumentException(
format(
"Cannot determine size of a subclass. %s extends from %s",
clazz.getSimpleName(), clazz.getSuperclass().getSimpleName()));
Messages.format(
"error.common.cannot_determine_size_subclass",
clazz.getSimpleName(),
clazz.getSuperclass().getSimpleName()));
}

long size = RamUsageEstimator.shallowSizeOf(clazz);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.apache.tsfile.block.column;

import org.apache.tsfile.i18n.Messages;

import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
Expand Down Expand Up @@ -66,7 +68,8 @@ private static ColumnEncoding getColumnEncoding(byte value) {
case 5:
return DICTIONARY;
default:
throw new IllegalArgumentException("Invalid value: " + value);
throw new IllegalArgumentException(
Messages.format("error.common.invalid_column_encoding_value", value));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.apache.tsfile.enums;

import org.apache.tsfile.i18n.Messages;
import org.apache.tsfile.utils.Binary;
import org.apache.tsfile.write.UnSupportedDataTypeException;

Expand Down Expand Up @@ -193,7 +194,7 @@ public static TSDataType getTsDataType(byte type) {
case 12:
return TSDataType.OBJECT;
default:
throw new IllegalArgumentException("Invalid input: " + type);
throw new IllegalArgumentException(Messages.format("error.common.invalid_input", type));
}
}

Expand Down Expand Up @@ -325,7 +326,7 @@ public Object castFromSingleValue(TSDataType sourceType, Object value) {
break;
}
throw new ClassCastException(
String.format("Unsupported cast: from %s to %s", sourceType, this));
Messages.format("error.common.unsupported_cast", sourceType, this));
}

@SuppressWarnings({"java:S3012", "java:S3776", "java:S6541"})
Expand Down Expand Up @@ -472,7 +473,7 @@ public Object castFromArray(TSDataType sourceType, Object array) {
break;
}
throw new ClassCastException(
String.format("Unsupported cast: from %s to %s", sourceType, this));
Messages.format("error.common.unsupported_cast", sourceType, this));
}

public static TSDataType deserializeFrom(ByteBuffer buffer) {
Expand Down
120 changes: 120 additions & 0 deletions java/common/src/main/java/org/apache/tsfile/i18n/Messages.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.tsfile.i18n;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;

/**
* Resolves log and exception message patterns from locale-specific resource bundles.
*
* <p>Locale resolution order (evaluated once at class load):
*
* <ol>
* <li>System property {@code -Dtsfile.locale} (e.g. {@code zh}, {@code zh-CN}).
* <li>{@link Locale#getDefault()}.
* <li>English fallback via {@link ResourceBundle}'s root-bundle mechanism.
* </ol>
*
* <p>Use {@link #get(String)} for SLF4J patterns (which contain {@code &#123;&#125;} placeholders
* and are formatted lazily by SLF4J), and {@link #format(String, Object...)} for exception messages
* (which use {@link String#format} positional placeholders like {@code %1$s}).
*/
public final class Messages {

private static final String BUNDLE_BASE_NAME = "org.apache.tsfile.i18n.messages";
private static final ResourceBundle BUNDLE = loadBundle();

private Messages() {}

/**
* Returns the raw message pattern.
*
* @throws java.util.MissingResourceException if {@code key} is not defined in the bundle
*/
public static String get(String key) {
return BUNDLE.getString(key);
}

/**
* Returns the message pattern formatted with the given arguments via {@link String#format}.
*
* @throws java.util.MissingResourceException if {@code key} is not defined in the bundle
* @throws java.util.IllegalFormatException if the pattern and {@code args} are incompatible
*/
public static String format(String key, Object... args) {
return String.format(BUNDLE.getString(key), args);
}

private static ResourceBundle loadBundle() {
return ResourceBundle.getBundle(
BUNDLE_BASE_NAME, determineLocale(), Messages.class.getClassLoader(), new Utf8Control());
}

private static Locale determineLocale() {
String prop = System.getProperty("tsfile.locale");
if (prop != null && !prop.isEmpty()) {
return Locale.forLanguageTag(prop);
}
return Locale.getDefault();
}

/**
* Loads {@code .properties} files as UTF-8 instead of the JDK default ISO-8859-1. Lets us write
* Chinese characters directly in {@code messages_zh.properties} without Unicode escapes.
*/
private static final class Utf8Control extends ResourceBundle.Control {
/**
* Disable Java's default fallback to {@link Locale#getDefault()}. Without this override, when a
* user requests "en" (via {@code -Dtsfile.locale=en}) on a JVM whose default locale is "zh",
* ResourceBundle's default behavior is to ALSO load the zh bundle as a parent, which causes
* {@code BUNDLE.getString(key)} to return Chinese text. Returning {@code null} keeps the
* resolution strictly within the requested locale's candidate chain (e.g., {@code [en, ROOT]}).
*/
@Override
public Locale getFallbackLocale(String baseName, Locale locale) {
return null;
}

@Override
public ResourceBundle newBundle(
String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
throws IOException, IllegalAccessException, InstantiationException {
if (!"java.properties".equals(format)) {
return super.newBundle(baseName, locale, format, loader, reload);
}
String bundleName = toBundleName(baseName, locale);
String resourceName = toResourceName(bundleName, "properties");
try (InputStream in = loader.getResourceAsStream(resourceName)) {
if (in == null) {
return null;
}
try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
return new PropertyResourceBundle(reader);
}
}
}
}
}
21 changes: 13 additions & 8 deletions java/common/src/main/java/org/apache/tsfile/utils/BitMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.apache.tsfile.utils;

import org.apache.tsfile.i18n.Messages;

import java.util.Arrays;
import java.util.Objects;

Expand Down Expand Up @@ -81,7 +83,8 @@ public void markRange(int startPosition, int length) {

if (startPosition < 0 || startPosition + length > size) {
throw new IndexOutOfBoundsException(
"startPosition " + startPosition + " + length " + length + " is out of range " + size);
Messages.format(
"error.common.bitmap_start_length_out_of_range", startPosition, length, size));
}

int bitEnd = startPosition + length - 1;
Expand Down Expand Up @@ -118,7 +121,8 @@ public void unmarkRange(int startPosition, int length) {

if (startPosition < 0 || startPosition + length > size) {
throw new IndexOutOfBoundsException(
"startPosition " + startPosition + " + length " + length + " is out of range " + size);
Messages.format(
"error.common.bitmap_start_length_out_of_range", startPosition, length, size));
}

int bitEnd = startPosition + length - 1;
Expand Down Expand Up @@ -267,10 +271,10 @@ public boolean equalsInRange(Object obj, int rangeSize) {
BitMap other = (BitMap) obj;
if (rangeSize > size || rangeSize > other.size) {
throw new IllegalArgumentException(
"range size "
+ rangeSize
+ " should <= the minimal bitmap size "
+ Math.min(this.size, other.size));
Messages.format(
"error.common.bitmap_range_size_exceeds",
rangeSize,
Math.min(this.size, other.size)));
}

int byteSize = rangeSize / Byte.SIZE;
Expand Down Expand Up @@ -314,10 +318,11 @@ public BitMap clone() {
public static void copyOfRange(BitMap src, int srcPos, BitMap dest, int destPos, int length) {
if (srcPos + length > src.size) {
throw new IndexOutOfBoundsException(
(srcPos + length - 1) + " is out of src range " + src.size);
Messages.format("error.common.bitmap_out_of_src_range", (srcPos + length - 1), src.size));
} else if (destPos + length > dest.size) {
throw new IndexOutOfBoundsException(
(destPos + length - 1) + " is out of dest range " + dest.size);
Messages.format(
"error.common.bitmap_out_of_dest_range", (destPos + length - 1), dest.size));
}
for (int i = 0; i < length; ++i) {
if (src.isMarked(srcPos + i)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.apache.tsfile.utils;

import org.apache.tsfile.i18n.Messages;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
Expand Down Expand Up @@ -565,7 +567,7 @@ public static long shallowSizeOf(Object obj) {
*/
public static long shallowSizeOfInstance(Class<?> clazz) {
if (clazz.isArray())
throw new IllegalArgumentException("This method does not work with array classes.");
throw new IllegalArgumentException(Messages.get("error.common.no_array_classes"));
if (clazz.isPrimitive()) return primitiveSizes.get(clazz);

long size = NUM_BYTES_OBJECT_HEADER;
Expand Down
Loading
Loading